@bpmnkit/canvas 0.0.8
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/README.md +128 -0
- package/dist/canvas.d.ts +114 -0
- package/dist/canvas.js +326 -0
- package/dist/css.d.ts +10 -0
- package/dist/css.js +226 -0
- package/dist/index.d.ts +72 -0
- package/dist/index.js +72 -0
- package/dist/keyboard.d.ts +40 -0
- package/dist/keyboard.js +166 -0
- package/dist/minimap.d.ts +37 -0
- package/dist/minimap.js +171 -0
- package/dist/renderer.d.ts +40 -0
- package/dist/renderer.js +835 -0
- package/dist/types.d.ts +177 -0
- package/dist/types.js +2 -0
- package/dist/viewport.d.ts +77 -0
- package/dist/viewport.js +203 -0
- package/package.json +30 -0
package/README.md
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
<img src="https://raw.githubusercontent.com/bpmn-sdk/monorepo/main/doc/logos/logo-2-gateway.svg" width="72" height="72" alt="BPMN Kit logo">
|
|
3
|
+
<h1>@bpmnkit/canvas</h1>
|
|
4
|
+
<p>Zero-dependency SVG BPMN viewer with pan/zoom, theming, and a plugin API</p>
|
|
5
|
+
|
|
6
|
+
[](https://www.npmjs.com/package/@bpmnkit/canvas)
|
|
7
|
+
[](https://github.com/bpmnkit/monorepo/blob/main/LICENSE)
|
|
8
|
+
[](https://github.com/bpmnkit/monorepo)
|
|
9
|
+
|
|
10
|
+
[Documentation](https://bpmn-sdk-docs.pages.dev) · [GitHub](https://github.com/bpmnkit/monorepo) · [Changelog](https://github.com/bpmnkit/monorepo/blob/main/packages/canvas/CHANGELOG.md)
|
|
11
|
+
</div>
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Overview
|
|
16
|
+
|
|
17
|
+
`@bpmnkit/canvas` renders BPMN 2.0 diagrams as interactive SVG. It has zero runtime dependencies, works in any framework (or none), and exposes a plugin API for extending its behaviour.
|
|
18
|
+
|
|
19
|
+
## Features
|
|
20
|
+
|
|
21
|
+
- **SVG rendering** — crisp diagrams at any zoom level, all element types
|
|
22
|
+
- **Pan & zoom** — mouse drag, scroll wheel, touch/pinch, keyboard shortcuts
|
|
23
|
+
- **Theming** — light / dark / system-auto, fully overridable via CSS custom properties
|
|
24
|
+
- **Plugin API** — install composable `CanvasPlugin` add-ons without touching core code
|
|
25
|
+
- **Event system** — subscribe to element clicks, selection changes, diagram load, viewport updates
|
|
26
|
+
- **Keyboard navigation** — arrow keys, fit-to-screen, zoom shortcuts
|
|
27
|
+
- **Zero dependencies** — runs in browsers, bundlers, and SSR
|
|
28
|
+
|
|
29
|
+
## Installation
|
|
30
|
+
|
|
31
|
+
```sh
|
|
32
|
+
npm install @bpmnkit/canvas
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Quick Start
|
|
36
|
+
|
|
37
|
+
```typescript
|
|
38
|
+
import { BpmnCanvas } from "@bpmnkit/canvas"
|
|
39
|
+
|
|
40
|
+
const canvas = new BpmnCanvas({
|
|
41
|
+
container: document.getElementById("canvas")!,
|
|
42
|
+
theme: "dark", // "light" | "dark" | "auto"
|
|
43
|
+
grid: true, // dot-grid background
|
|
44
|
+
plugins: [], // CanvasPlugin[]
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
// Load a diagram
|
|
48
|
+
canvas.loadXML(bpmnXml)
|
|
49
|
+
|
|
50
|
+
// Respond to element clicks
|
|
51
|
+
canvas.on("element:click", (id, event) => {
|
|
52
|
+
console.log("Clicked:", id)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
// Fit diagram to container
|
|
56
|
+
canvas.fit()
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## API Reference
|
|
60
|
+
|
|
61
|
+
### Constructor Options
|
|
62
|
+
|
|
63
|
+
```typescript
|
|
64
|
+
interface CanvasOptions {
|
|
65
|
+
container: HTMLElement
|
|
66
|
+
theme?: "light" | "dark" | "auto" // default: "auto"
|
|
67
|
+
grid?: boolean // default: false
|
|
68
|
+
fit?: boolean // auto-fit on load, default: true
|
|
69
|
+
plugins?: CanvasPlugin[]
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### CanvasApi Methods
|
|
74
|
+
|
|
75
|
+
| Method | Description |
|
|
76
|
+
|--------|-------------|
|
|
77
|
+
| `loadXML(xml)` | Parse and render a BPMN XML string |
|
|
78
|
+
| `fit()` | Fit the diagram to the container |
|
|
79
|
+
| `zoom(factor)` | Set zoom level (1 = 100%) |
|
|
80
|
+
| `getShapes()` | All rendered `RenderedShape` objects |
|
|
81
|
+
| `getEdges()` | All rendered `RenderedEdge` objects |
|
|
82
|
+
| `setTheme(theme)` | Switch theme at runtime |
|
|
83
|
+
| `destroy()` | Remove canvas and clean up |
|
|
84
|
+
| `on(event, handler)` | Subscribe to a canvas event |
|
|
85
|
+
| `off(event, handler)` | Unsubscribe |
|
|
86
|
+
|
|
87
|
+
### Events
|
|
88
|
+
|
|
89
|
+
| Event | Payload | Description |
|
|
90
|
+
|-------|---------|-------------|
|
|
91
|
+
| `diagram:load` | `BpmnDefinitions` | Fired when a new diagram loads |
|
|
92
|
+
| `element:click` | `(id, PointerEvent)` | An element was clicked |
|
|
93
|
+
| `editor:select` | `string[]` | Selection changed (element IDs) |
|
|
94
|
+
| `viewport:change` | `ViewportState` | Pan or zoom occurred |
|
|
95
|
+
|
|
96
|
+
### Plugin API
|
|
97
|
+
|
|
98
|
+
```typescript
|
|
99
|
+
import type { CanvasPlugin, CanvasApi } from "@bpmnkit/canvas"
|
|
100
|
+
|
|
101
|
+
const myPlugin: CanvasPlugin = {
|
|
102
|
+
name: "my-plugin",
|
|
103
|
+
install(api: CanvasApi) {
|
|
104
|
+
api.on("element:click", (id) => console.log("clicked", id))
|
|
105
|
+
},
|
|
106
|
+
uninstall() {},
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## Related Packages
|
|
113
|
+
|
|
114
|
+
| Package | Description |
|
|
115
|
+
|---------|-------------|
|
|
116
|
+
| [`@bpmnkit/core`](https://www.npmjs.com/package/@bpmnkit/core) | BPMN/DMN/Form parser, builder, layout engine |
|
|
117
|
+
| [`@bpmnkit/editor`](https://www.npmjs.com/package/@bpmnkit/editor) | Full-featured interactive BPMN editor |
|
|
118
|
+
| [`@bpmnkit/engine`](https://www.npmjs.com/package/@bpmnkit/engine) | Lightweight BPMN process execution engine |
|
|
119
|
+
| [`@bpmnkit/feel`](https://www.npmjs.com/package/@bpmnkit/feel) | FEEL expression language parser & evaluator |
|
|
120
|
+
| [`@bpmnkit/plugins`](https://www.npmjs.com/package/@bpmnkit/plugins) | 22 composable canvas plugins |
|
|
121
|
+
| [`@bpmnkit/api`](https://www.npmjs.com/package/@bpmnkit/api) | Camunda 8 REST API TypeScript client |
|
|
122
|
+
| [`@bpmnkit/ascii`](https://www.npmjs.com/package/@bpmnkit/ascii) | Render BPMN diagrams as Unicode ASCII art |
|
|
123
|
+
| [`@bpmnkit/profiles`](https://www.npmjs.com/package/@bpmnkit/profiles) | Shared auth, profile storage, and client factories for CLI & proxy |
|
|
124
|
+
| [`@bpmnkit/operate`](https://www.npmjs.com/package/@bpmnkit/operate) | Monitoring & operations frontend for Camunda clusters |
|
|
125
|
+
|
|
126
|
+
## License
|
|
127
|
+
|
|
128
|
+
[MIT](https://github.com/bpmnkit/monorepo/blob/main/LICENSE) © bpmn-sdk
|
package/dist/canvas.d.ts
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import type { BpmnDefinitions } from "@bpmnkit/core";
|
|
2
|
+
import type { CanvasEvents, CanvasOptions, Theme } from "./types.js";
|
|
3
|
+
/**
|
|
4
|
+
* BpmnCanvas — a high-performance, accessible BPMN 2.0 diagram viewer.
|
|
5
|
+
*
|
|
6
|
+
* ## Quick start
|
|
7
|
+
* ```typescript
|
|
8
|
+
* import { BpmnCanvas } from "@bpmnkit/canvas";
|
|
9
|
+
*
|
|
10
|
+
* const canvas = new BpmnCanvas({
|
|
11
|
+
* container: document.getElementById("app")!,
|
|
12
|
+
* xml: myBpmnXml,
|
|
13
|
+
* theme: "auto",
|
|
14
|
+
* });
|
|
15
|
+
* ```
|
|
16
|
+
*
|
|
17
|
+
* ## Framework integration
|
|
18
|
+
* The canvas is framework-agnostic and mounts into any `HTMLElement`.
|
|
19
|
+
*
|
|
20
|
+
* ### React
|
|
21
|
+
* ```tsx
|
|
22
|
+
* const ref = useRef<HTMLDivElement>(null);
|
|
23
|
+
* useEffect(() => {
|
|
24
|
+
* const canvas = new BpmnCanvas({ container: ref.current!, xml });
|
|
25
|
+
* return () => canvas.destroy();
|
|
26
|
+
* }, [xml]);
|
|
27
|
+
* return <div ref={ref} style={{ width: "100%", height: "500px" }} />;
|
|
28
|
+
* ```
|
|
29
|
+
*
|
|
30
|
+
* ### Vue
|
|
31
|
+
* ```vue
|
|
32
|
+
* <template><div ref="el" style="width:100%;height:500px" /></template>
|
|
33
|
+
* <script setup>
|
|
34
|
+
* const el = ref(null);
|
|
35
|
+
* onMounted(() => { canvas = new BpmnCanvas({ container: el.value, xml }); });
|
|
36
|
+
* onUnmounted(() => canvas?.destroy());
|
|
37
|
+
* </script>
|
|
38
|
+
* ```
|
|
39
|
+
*
|
|
40
|
+
* ## Plugin system
|
|
41
|
+
* Extend the canvas with custom behaviour by passing plugins to the constructor:
|
|
42
|
+
* ```typescript
|
|
43
|
+
* const canvas = new BpmnCanvas({
|
|
44
|
+
* container,
|
|
45
|
+
* plugins: [tooltipPlugin, editModePlugin],
|
|
46
|
+
* });
|
|
47
|
+
* ```
|
|
48
|
+
* See {@link CanvasPlugin} for the plugin contract.
|
|
49
|
+
*/
|
|
50
|
+
export declare class BpmnCanvas {
|
|
51
|
+
private readonly _id;
|
|
52
|
+
private readonly _host;
|
|
53
|
+
private readonly _svg;
|
|
54
|
+
private readonly _viewportG;
|
|
55
|
+
private readonly _containersG;
|
|
56
|
+
private readonly _edgesG;
|
|
57
|
+
private readonly _shapesG;
|
|
58
|
+
private readonly _labelsG;
|
|
59
|
+
private _gridPattern;
|
|
60
|
+
private _markerId;
|
|
61
|
+
private readonly _viewport;
|
|
62
|
+
private readonly _keyboard;
|
|
63
|
+
private readonly _plugins;
|
|
64
|
+
private _shapes;
|
|
65
|
+
private _edges;
|
|
66
|
+
private _currentDefs;
|
|
67
|
+
private _theme;
|
|
68
|
+
private _fit;
|
|
69
|
+
private _listeners;
|
|
70
|
+
constructor(options: CanvasOptions);
|
|
71
|
+
private _ro;
|
|
72
|
+
/**
|
|
73
|
+
* Parses and renders a BPMN 2.0 XML string.
|
|
74
|
+
*
|
|
75
|
+
* @throws {Error} If the XML cannot be parsed.
|
|
76
|
+
*/
|
|
77
|
+
load(xml: string): void;
|
|
78
|
+
/**
|
|
79
|
+
* Renders an already-parsed `BpmnDefinitions` model.
|
|
80
|
+
* Use this when you already have the parsed model from `@bpmnkit/core`.
|
|
81
|
+
*/
|
|
82
|
+
loadDefinitions(defs: BpmnDefinitions): void;
|
|
83
|
+
/** Clears the canvas and fires `diagram:clear`. */
|
|
84
|
+
clear(): void;
|
|
85
|
+
/**
|
|
86
|
+
* Scales and pans the viewport to make the entire diagram visible.
|
|
87
|
+
* @param padding — pixels of whitespace around the diagram. Default: 40.
|
|
88
|
+
*/
|
|
89
|
+
fitView(padding?: number): void;
|
|
90
|
+
/** Sets the color theme. Pass `"auto"` to follow the OS preference. */
|
|
91
|
+
setTheme(theme: Theme): void;
|
|
92
|
+
/** Zooms in by 25% centred on the canvas. */
|
|
93
|
+
zoomIn(): void;
|
|
94
|
+
/** Zooms out by 25% centred on the canvas. */
|
|
95
|
+
zoomOut(): void;
|
|
96
|
+
/** Resets to 100% zoom, centred on the canvas. */
|
|
97
|
+
resetZoom(): void;
|
|
98
|
+
/**
|
|
99
|
+
* Subscribes to a canvas event. Returns an unsubscribe function.
|
|
100
|
+
*
|
|
101
|
+
* @example
|
|
102
|
+
* ```typescript
|
|
103
|
+
* const off = canvas.on("element:click", (id) => console.log(id));
|
|
104
|
+
* off(); // unsubscribe
|
|
105
|
+
* ```
|
|
106
|
+
*/
|
|
107
|
+
on<K extends keyof CanvasEvents>(event: K, handler: CanvasEvents[K]): () => void;
|
|
108
|
+
/** Destroys the canvas, removing all DOM nodes and event listeners. */
|
|
109
|
+
destroy(): void;
|
|
110
|
+
private _applyTheme;
|
|
111
|
+
private _installPlugin;
|
|
112
|
+
private _emit;
|
|
113
|
+
}
|
|
114
|
+
//# sourceMappingURL=canvas.d.ts.map
|
package/dist/canvas.js
ADDED
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
import { Bpmn } from "@bpmnkit/core";
|
|
2
|
+
import { injectStyles } from "./css.js";
|
|
3
|
+
import { KeyboardHandler } from "./keyboard.js";
|
|
4
|
+
import { computeDiagramBounds, createDefs, createGrid, render } from "./renderer.js";
|
|
5
|
+
import { ViewportController } from "./viewport.js";
|
|
6
|
+
const NS = "http://www.w3.org/2000/svg";
|
|
7
|
+
let _instanceCounter = 0;
|
|
8
|
+
/**
|
|
9
|
+
* BpmnCanvas — a high-performance, accessible BPMN 2.0 diagram viewer.
|
|
10
|
+
*
|
|
11
|
+
* ## Quick start
|
|
12
|
+
* ```typescript
|
|
13
|
+
* import { BpmnCanvas } from "@bpmnkit/canvas";
|
|
14
|
+
*
|
|
15
|
+
* const canvas = new BpmnCanvas({
|
|
16
|
+
* container: document.getElementById("app")!,
|
|
17
|
+
* xml: myBpmnXml,
|
|
18
|
+
* theme: "auto",
|
|
19
|
+
* });
|
|
20
|
+
* ```
|
|
21
|
+
*
|
|
22
|
+
* ## Framework integration
|
|
23
|
+
* The canvas is framework-agnostic and mounts into any `HTMLElement`.
|
|
24
|
+
*
|
|
25
|
+
* ### React
|
|
26
|
+
* ```tsx
|
|
27
|
+
* const ref = useRef<HTMLDivElement>(null);
|
|
28
|
+
* useEffect(() => {
|
|
29
|
+
* const canvas = new BpmnCanvas({ container: ref.current!, xml });
|
|
30
|
+
* return () => canvas.destroy();
|
|
31
|
+
* }, [xml]);
|
|
32
|
+
* return <div ref={ref} style={{ width: "100%", height: "500px" }} />;
|
|
33
|
+
* ```
|
|
34
|
+
*
|
|
35
|
+
* ### Vue
|
|
36
|
+
* ```vue
|
|
37
|
+
* <template><div ref="el" style="width:100%;height:500px" /></template>
|
|
38
|
+
* <script setup>
|
|
39
|
+
* const el = ref(null);
|
|
40
|
+
* onMounted(() => { canvas = new BpmnCanvas({ container: el.value, xml }); });
|
|
41
|
+
* onUnmounted(() => canvas?.destroy());
|
|
42
|
+
* </script>
|
|
43
|
+
* ```
|
|
44
|
+
*
|
|
45
|
+
* ## Plugin system
|
|
46
|
+
* Extend the canvas with custom behaviour by passing plugins to the constructor:
|
|
47
|
+
* ```typescript
|
|
48
|
+
* const canvas = new BpmnCanvas({
|
|
49
|
+
* container,
|
|
50
|
+
* plugins: [tooltipPlugin, editModePlugin],
|
|
51
|
+
* });
|
|
52
|
+
* ```
|
|
53
|
+
* See {@link CanvasPlugin} for the plugin contract.
|
|
54
|
+
*/
|
|
55
|
+
export class BpmnCanvas {
|
|
56
|
+
// ── DOM structure ─────────────────────────────────────────────────
|
|
57
|
+
_id;
|
|
58
|
+
_host;
|
|
59
|
+
_svg;
|
|
60
|
+
_viewportG;
|
|
61
|
+
_containersG;
|
|
62
|
+
_edgesG;
|
|
63
|
+
_shapesG;
|
|
64
|
+
_labelsG;
|
|
65
|
+
_gridPattern = null;
|
|
66
|
+
_markerId = "";
|
|
67
|
+
// ── Sub-systems ───────────────────────────────────────────────────
|
|
68
|
+
_viewport;
|
|
69
|
+
_keyboard;
|
|
70
|
+
_plugins = [];
|
|
71
|
+
// ── State ─────────────────────────────────────────────────────────
|
|
72
|
+
_shapes = [];
|
|
73
|
+
_edges = [];
|
|
74
|
+
_currentDefs = null;
|
|
75
|
+
_theme;
|
|
76
|
+
_fit;
|
|
77
|
+
// ── Event emitter ─────────────────────────────────────────────────
|
|
78
|
+
_listeners = new Map();
|
|
79
|
+
constructor(options) {
|
|
80
|
+
injectStyles();
|
|
81
|
+
this._id = String(_instanceCounter++);
|
|
82
|
+
this._theme = options.theme ?? "auto";
|
|
83
|
+
this._fit = options.fit ?? "contain";
|
|
84
|
+
// ── Build DOM ────────────────────────────────────────────────
|
|
85
|
+
const container = options.container;
|
|
86
|
+
container.innerHTML = "";
|
|
87
|
+
this._host = document.createElement("div");
|
|
88
|
+
this._host.className = "bpmnkit-canvas-host";
|
|
89
|
+
this._host.setAttribute("role", "application");
|
|
90
|
+
this._host.setAttribute("aria-label", "BPMN Diagram");
|
|
91
|
+
this._host.setAttribute("tabindex", "0");
|
|
92
|
+
this._applyTheme(this._theme);
|
|
93
|
+
container.appendChild(this._host);
|
|
94
|
+
// SVG root
|
|
95
|
+
this._svg = document.createElementNS(NS, "svg");
|
|
96
|
+
this._svg.setAttribute("aria-hidden", "true");
|
|
97
|
+
this._host.appendChild(this._svg);
|
|
98
|
+
// Arrow marker defs
|
|
99
|
+
this._markerId = createDefs(this._svg, this._id);
|
|
100
|
+
// Dot grid
|
|
101
|
+
if (options.grid !== false) {
|
|
102
|
+
this._gridPattern = createGrid(this._svg, this._id);
|
|
103
|
+
}
|
|
104
|
+
// Viewport group
|
|
105
|
+
this._viewportG = document.createElementNS(NS, "g");
|
|
106
|
+
this._svg.appendChild(this._viewportG);
|
|
107
|
+
// Layer order: containers (pools/lanes) → edges → shapes → labels
|
|
108
|
+
this._containersG = document.createElementNS(NS, "g");
|
|
109
|
+
this._edgesG = document.createElementNS(NS, "g");
|
|
110
|
+
this._shapesG = document.createElementNS(NS, "g");
|
|
111
|
+
this._labelsG = document.createElementNS(NS, "g");
|
|
112
|
+
this._viewportG.appendChild(this._containersG);
|
|
113
|
+
this._viewportG.appendChild(this._edgesG);
|
|
114
|
+
this._viewportG.appendChild(this._shapesG);
|
|
115
|
+
this._viewportG.appendChild(this._labelsG);
|
|
116
|
+
// ── Viewport controller ───────────────────────────────────────
|
|
117
|
+
this._viewport = new ViewportController(this._host, this._svg, this._viewportG, this._gridPattern, (state) => {
|
|
118
|
+
this._emit("viewport:change", state);
|
|
119
|
+
});
|
|
120
|
+
// ── Keyboard ──────────────────────────────────────────────────
|
|
121
|
+
this._keyboard = new KeyboardHandler(this._host, this._viewport, () => this.fitView(), (id) => {
|
|
122
|
+
const shape = this._shapes.find((s) => s.id === id);
|
|
123
|
+
if (shape) {
|
|
124
|
+
const e = new PointerEvent("click");
|
|
125
|
+
this._emit("element:click", id, e);
|
|
126
|
+
}
|
|
127
|
+
}, (id) => this._emit("element:focus", id), () => this._emit("element:blur"));
|
|
128
|
+
// ── Click detection ───────────────────────────────────────────
|
|
129
|
+
// Use elementFromPoint rather than e.target because native SVG hit-testing
|
|
130
|
+
// can return the root <svg> element even when a shape is visually under the
|
|
131
|
+
// cursor (reproducible when the canvas is inside a flex/scroll container).
|
|
132
|
+
// elementFromPoint correctly resolves the topmost painted element.
|
|
133
|
+
this._svg.addEventListener("click", (e) => {
|
|
134
|
+
if (this._viewport.didPan)
|
|
135
|
+
return;
|
|
136
|
+
// elementFromPoint resolves the correct element when native SVG hit-testing
|
|
137
|
+
// returns the root <svg> (reproducible in flex/scroll containers). Fall back
|
|
138
|
+
// to e.target for test environments where elementFromPoint isn't reliable.
|
|
139
|
+
const fromPoint = document.elementFromPoint(e.clientX, e.clientY);
|
|
140
|
+
const target = fromPoint?.closest("[data-bpmnkit-id]") ??
|
|
141
|
+
e.target.closest("[data-bpmnkit-id]");
|
|
142
|
+
const id = target?.getAttribute("data-bpmnkit-id");
|
|
143
|
+
if (id)
|
|
144
|
+
this._emit("element:click", id, e);
|
|
145
|
+
});
|
|
146
|
+
// ── Install plugins ───────────────────────────────────────────
|
|
147
|
+
if (options.plugins) {
|
|
148
|
+
for (const plugin of options.plugins) {
|
|
149
|
+
this._installPlugin(plugin);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
// ── Initial diagram ───────────────────────────────────────────
|
|
153
|
+
if (options.xml) {
|
|
154
|
+
this.load(options.xml);
|
|
155
|
+
}
|
|
156
|
+
// Re-fit on container resize
|
|
157
|
+
const ro = new ResizeObserver(() => {
|
|
158
|
+
if (this._currentDefs)
|
|
159
|
+
this.fitView();
|
|
160
|
+
});
|
|
161
|
+
ro.observe(this._host);
|
|
162
|
+
this._ro = ro;
|
|
163
|
+
}
|
|
164
|
+
_ro;
|
|
165
|
+
// ── Public API ────────────────────────────────────────────────────
|
|
166
|
+
/**
|
|
167
|
+
* Parses and renders a BPMN 2.0 XML string.
|
|
168
|
+
*
|
|
169
|
+
* @throws {Error} If the XML cannot be parsed.
|
|
170
|
+
*/
|
|
171
|
+
load(xml) {
|
|
172
|
+
const defs = Bpmn.parse(xml);
|
|
173
|
+
this.loadDefinitions(defs);
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Renders an already-parsed `BpmnDefinitions` model.
|
|
177
|
+
* Use this when you already have the parsed model from `@bpmnkit/core`.
|
|
178
|
+
*/
|
|
179
|
+
loadDefinitions(defs) {
|
|
180
|
+
// Clear previous content
|
|
181
|
+
this._containersG.innerHTML = "";
|
|
182
|
+
this._edgesG.innerHTML = "";
|
|
183
|
+
this._shapesG.innerHTML = "";
|
|
184
|
+
this._labelsG.innerHTML = "";
|
|
185
|
+
this._shapes = [];
|
|
186
|
+
this._edges = [];
|
|
187
|
+
this._currentDefs = defs;
|
|
188
|
+
const result = render(defs, this._containersG, this._edgesG, this._shapesG, this._labelsG, this._markerId, this._id);
|
|
189
|
+
this._shapes = result.shapes;
|
|
190
|
+
this._edges = result.edges;
|
|
191
|
+
this._keyboard.setShapes(this._shapes);
|
|
192
|
+
if (this._fit !== "none") {
|
|
193
|
+
// Defer fit to next frame so the SVG has been laid out
|
|
194
|
+
requestAnimationFrame(() => this.fitView());
|
|
195
|
+
}
|
|
196
|
+
this._emit("diagram:load", defs);
|
|
197
|
+
}
|
|
198
|
+
/** Clears the canvas and fires `diagram:clear`. */
|
|
199
|
+
clear() {
|
|
200
|
+
this._containersG.innerHTML = "";
|
|
201
|
+
this._edgesG.innerHTML = "";
|
|
202
|
+
this._shapesG.innerHTML = "";
|
|
203
|
+
this._labelsG.innerHTML = "";
|
|
204
|
+
this._shapes = [];
|
|
205
|
+
this._edges = [];
|
|
206
|
+
this._currentDefs = null;
|
|
207
|
+
this._emit("diagram:clear");
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Scales and pans the viewport to make the entire diagram visible.
|
|
211
|
+
* @param padding — pixels of whitespace around the diagram. Default: 40.
|
|
212
|
+
*/
|
|
213
|
+
fitView(padding = 40) {
|
|
214
|
+
if (!this._currentDefs)
|
|
215
|
+
return;
|
|
216
|
+
const bounds = computeDiagramBounds(this._currentDefs);
|
|
217
|
+
if (!bounds)
|
|
218
|
+
return;
|
|
219
|
+
const svgW = this._svg.clientWidth;
|
|
220
|
+
const svgH = this._svg.clientHeight;
|
|
221
|
+
if (svgW === 0 || svgH === 0)
|
|
222
|
+
return;
|
|
223
|
+
const dW = bounds.maxX - bounds.minX;
|
|
224
|
+
const dH = bounds.maxY - bounds.minY;
|
|
225
|
+
if (dW === 0 || dH === 0)
|
|
226
|
+
return;
|
|
227
|
+
const scaleX = (svgW - padding * 2) / dW;
|
|
228
|
+
const scaleY = (svgH - padding * 2) / dH;
|
|
229
|
+
let scale = Math.min(scaleX, scaleY);
|
|
230
|
+
if (this._fit === "center")
|
|
231
|
+
scale = 1;
|
|
232
|
+
const tx = (svgW - dW * scale) / 2 - bounds.minX * scale;
|
|
233
|
+
const ty = (svgH - dH * scale) / 2 - bounds.minY * scale;
|
|
234
|
+
this._viewport.set({ tx, ty, scale });
|
|
235
|
+
}
|
|
236
|
+
/** Sets the color theme. Pass `"auto"` to follow the OS preference. */
|
|
237
|
+
setTheme(theme) {
|
|
238
|
+
this._theme = theme;
|
|
239
|
+
this._applyTheme(theme);
|
|
240
|
+
}
|
|
241
|
+
/** Zooms in by 25% centred on the canvas. */
|
|
242
|
+
zoomIn() {
|
|
243
|
+
const { width, height } = this._svg.getBoundingClientRect();
|
|
244
|
+
this._viewport.zoomAt(width / 2, height / 2, 1.25);
|
|
245
|
+
}
|
|
246
|
+
/** Zooms out by 25% centred on the canvas. */
|
|
247
|
+
zoomOut() {
|
|
248
|
+
const { width, height } = this._svg.getBoundingClientRect();
|
|
249
|
+
this._viewport.zoomAt(width / 2, height / 2, 0.8);
|
|
250
|
+
}
|
|
251
|
+
/** Resets to 100% zoom, centred on the canvas. */
|
|
252
|
+
resetZoom() {
|
|
253
|
+
const { width, height } = this._svg.getBoundingClientRect();
|
|
254
|
+
this._viewport.set({ scale: 1, tx: width / 2, ty: height / 2 });
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Subscribes to a canvas event. Returns an unsubscribe function.
|
|
258
|
+
*
|
|
259
|
+
* @example
|
|
260
|
+
* ```typescript
|
|
261
|
+
* const off = canvas.on("element:click", (id) => console.log(id));
|
|
262
|
+
* off(); // unsubscribe
|
|
263
|
+
* ```
|
|
264
|
+
*/
|
|
265
|
+
on(event, handler) {
|
|
266
|
+
let set = this._listeners.get(event);
|
|
267
|
+
if (!set) {
|
|
268
|
+
set = new Set();
|
|
269
|
+
this._listeners.set(event, set);
|
|
270
|
+
}
|
|
271
|
+
set.add(handler);
|
|
272
|
+
return () => set.delete(handler);
|
|
273
|
+
}
|
|
274
|
+
/** Destroys the canvas, removing all DOM nodes and event listeners. */
|
|
275
|
+
destroy() {
|
|
276
|
+
this._ro.disconnect();
|
|
277
|
+
this._viewport.destroy();
|
|
278
|
+
this._keyboard.destroy();
|
|
279
|
+
for (const plugin of this._plugins)
|
|
280
|
+
plugin.uninstall?.();
|
|
281
|
+
this._plugins.length = 0;
|
|
282
|
+
this._listeners.clear();
|
|
283
|
+
this._host.remove();
|
|
284
|
+
}
|
|
285
|
+
// ── Private ───────────────────────────────────────────────────────
|
|
286
|
+
_applyTheme(theme) {
|
|
287
|
+
const resolved = theme === "auto"
|
|
288
|
+
? window.matchMedia("(prefers-color-scheme: dark)").matches
|
|
289
|
+
? "dark"
|
|
290
|
+
: "light"
|
|
291
|
+
: theme;
|
|
292
|
+
if (resolved === "dark") {
|
|
293
|
+
this._host.setAttribute("data-theme", "dark");
|
|
294
|
+
}
|
|
295
|
+
else {
|
|
296
|
+
this._host.removeAttribute("data-theme");
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
_installPlugin(plugin) {
|
|
300
|
+
this._plugins.push(plugin);
|
|
301
|
+
const api = {
|
|
302
|
+
container: this._host,
|
|
303
|
+
svg: this._svg,
|
|
304
|
+
viewportEl: this._viewportG,
|
|
305
|
+
getViewport: () => this._viewport.state,
|
|
306
|
+
setViewport: (s) => this._viewport.set(s),
|
|
307
|
+
getShapes: () => [...this._shapes],
|
|
308
|
+
getEdges: () => [...this._edges],
|
|
309
|
+
getTheme: () => this._theme,
|
|
310
|
+
setTheme: (theme) => this.setTheme(theme),
|
|
311
|
+
on: (event, handler) => this.on(event, handler),
|
|
312
|
+
emit: (event, ...args) => this._emit(event, ...args),
|
|
313
|
+
};
|
|
314
|
+
plugin.install(api);
|
|
315
|
+
}
|
|
316
|
+
_emit(event, ...args) {
|
|
317
|
+
const handlers = this._listeners.get(event);
|
|
318
|
+
if (!handlers)
|
|
319
|
+
return;
|
|
320
|
+
for (const h of handlers) {
|
|
321
|
+
;
|
|
322
|
+
h(...args);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
//# sourceMappingURL=canvas.js.map
|
package/dist/css.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** ID used to prevent duplicate style injection. */
|
|
2
|
+
export declare const STYLE_ID = "bpmnkit-canvas-styles-v1";
|
|
3
|
+
/** Complete CSS for the BpmnCanvas component, injected once into `<head>`. */
|
|
4
|
+
export declare const CANVAS_CSS = "\n.bpmnkit-canvas-host {\n position: relative;\n overflow: hidden;\n width: 100%;\n height: 100%;\n box-sizing: border-box;\n -webkit-user-select: none;\n user-select: none;\n touch-action: none;\n background: var(--bpmnkit-bg, #f8f9fa);\n}\n.bpmnkit-canvas-host *,\n.bpmnkit-canvas-host *::before,\n.bpmnkit-canvas-host *::after {\n box-sizing: border-box;\n}\n.bpmnkit-canvas-host > svg {\n display: block;\n width: 100%;\n height: 100%;\n cursor: default;\n}\n.bpmnkit-canvas-host.is-panning > svg {\n cursor: grabbing;\n}\n.bpmnkit-canvas-host > svg:focus {\n outline: none;\n}\n\n/* \u2500\u2500 Shapes \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n.bpmnkit-shape {\n cursor: pointer;\n outline: none;\n}\n.bpmnkit-shape:focus .bpmnkit-shape-body,\n.bpmnkit-shape:focus .bpmnkit-event-body,\n.bpmnkit-shape:focus .bpmnkit-gw-body,\n.bpmnkit-shape:focus .bpmnkit-pool-body,\n.bpmnkit-shape:focus .bpmnkit-lane-body {\n stroke: var(--bpmnkit-focus, #0066cc);\n stroke-width: 2.5;\n}\n.bpmnkit-shape.bpmnkit-selected .bpmnkit-shape-body,\n.bpmnkit-shape.bpmnkit-selected .bpmnkit-event-body,\n.bpmnkit-shape.bpmnkit-selected .bpmnkit-gw-body,\n.bpmnkit-shape.bpmnkit-selected .bpmnkit-pool-body,\n.bpmnkit-shape.bpmnkit-selected .bpmnkit-lane-body {\n stroke: var(--bpmnkit-highlight, #0066cc);\n stroke-width: 2.5;\n}\n\n.bpmnkit-shape-body,\n.bpmnkit-event-body,\n.bpmnkit-gw-body {\n fill: var(--bpmnkit-shape-fill, #ffffff);\n stroke: var(--bpmnkit-shape-stroke, #404040);\n stroke-width: 1.5;\n}\n.bpmnkit-end-body {\n fill: var(--bpmnkit-shape-fill, #ffffff);\n stroke: var(--bpmnkit-shape-stroke, #404040);\n stroke-width: 3;\n}\n.bpmnkit-event-inner {\n fill: none;\n stroke: var(--bpmnkit-shape-stroke, #404040);\n stroke-width: 1.5;\n}\n.bpmnkit-event-inner-dashed {\n fill: none;\n stroke: var(--bpmnkit-shape-stroke, #404040);\n stroke-width: 1.5;\n stroke-dasharray: 4 2;\n}\n.bpmnkit-eventsubprocess-body {\n fill: var(--bpmnkit-shape-fill, #ffffff);\n stroke: var(--bpmnkit-shape-stroke, #404040);\n stroke-width: 1.5;\n stroke-dasharray: 5 3;\n}\n.bpmnkit-callactivity-body {\n fill: var(--bpmnkit-shape-fill, #ffffff);\n stroke: var(--bpmnkit-shape-stroke, #404040);\n stroke-width: 3;\n}\n\n/* \u2500\u2500 Icons \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n.bpmnkit-icon {\n fill: none;\n stroke: var(--bpmnkit-shape-stroke, #404040);\n stroke-width: 1.5;\n stroke-linecap: round;\n stroke-linejoin: round;\n pointer-events: none;\n}\n.bpmnkit-icon-solid {\n fill: var(--bpmnkit-shape-stroke, #404040);\n stroke: none;\n pointer-events: none;\n}\n.bpmnkit-gw-marker {\n fill: var(--bpmnkit-shape-stroke, #404040);\n stroke: none;\n pointer-events: none;\n}\n.bpmnkit-gw-marker-stroke {\n fill: none;\n stroke: var(--bpmnkit-shape-stroke, #404040);\n stroke-width: 2.5;\n stroke-linecap: round;\n pointer-events: none;\n}\n\n/* \u2500\u2500 Pool / Lane \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n.bpmnkit-pool-body,\n.bpmnkit-lane-body {\n fill: var(--bpmnkit-shape-fill, #ffffff);\n stroke: var(--bpmnkit-shape-stroke, #404040);\n stroke-width: 1.5;\n}\n.bpmnkit-pool-header,\n.bpmnkit-lane-header {\n fill: var(--bpmnkit-pool-header, rgba(0,0,0,0.04));\n stroke: var(--bpmnkit-shape-stroke, #404040);\n stroke-width: 1.5;\n}\n[data-theme=\"dark\"] .bpmnkit-pool-header,\n[data-theme=\"dark\"] .bpmnkit-lane-header {\n fill: rgba(255,255,255,0.06);\n}\n\n/* \u2500\u2500 Message flow \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n.bpmnkit-msgflow-path {\n fill: none;\n stroke: var(--bpmnkit-flow-stroke, #404040);\n stroke-width: 1.5;\n stroke-dasharray: 6 3;\n}\n\n/* \u2500\u2500 Edges \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n.bpmnkit-edge {\n cursor: pointer;\n}\n.bpmnkit-edge-path {\n fill: none;\n stroke: var(--bpmnkit-flow-stroke, #404040);\n stroke-width: 1.5;\n}\n.bpmnkit-edge-assoc {\n fill: none;\n stroke: var(--bpmnkit-flow-stroke, #404040);\n stroke-width: 1.5;\n stroke-dasharray: 5 3;\n}\n.bpmnkit-edge-default-slash {\n stroke: var(--bpmnkit-flow-stroke, #404040);\n stroke-width: 1.5;\n}\n.bpmnkit-arrow-fill {\n fill: var(--bpmnkit-flow-stroke, #404040);\n}\n\n/* \u2500\u2500 Labels \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n.bpmnkit-label {\n fill: var(--bpmnkit-text, #333333);\n font-family: system-ui, -apple-system, sans-serif;\n font-size: 11px;\n text-anchor: middle;\n dominant-baseline: central;\n pointer-events: none;\n /* White halo behind text so labels never visually merge with edge lines */\n paint-order: stroke;\n stroke: var(--bpmnkit-bg, #f8f9fa);\n stroke-width: 4px;\n stroke-linejoin: round;\n}\n\n/* \u2500\u2500 Light theme (default) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n.bpmnkit-canvas-host {\n --bpmnkit-bg: #f8f9fa;\n --bpmnkit-grid: rgba(0, 0, 0, 0.14);\n --bpmnkit-shape-fill: #ffffff;\n --bpmnkit-shape-stroke: #404040;\n --bpmnkit-flow-stroke: #404040;\n --bpmnkit-text: var(--bpmnkit-fg, #333333);\n --bpmnkit-highlight: var(--bpmnkit-accent, #1a56db);\n --bpmnkit-focus: #0066cc;\n --bpmnkit-overlay-bg: rgba(248, 249, 250, 0.92);\n --bpmnkit-overlay-border: rgba(0, 0, 0, 0.12);\n}\n\n/* \u2500\u2500 Dark theme \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n.bpmnkit-canvas-host[data-theme=\"dark\"] {\n --bpmnkit-bg: #0d0d16;\n --bpmnkit-grid: rgba(255, 255, 255, 0.07);\n --bpmnkit-shape-fill: #1e1e2e;\n --bpmnkit-shape-stroke: #8888bb;\n --bpmnkit-flow-stroke: #7777aa;\n --bpmnkit-text: var(--bpmnkit-fg, #cdd6f4);\n --bpmnkit-highlight: var(--bpmnkit-accent-bright, #89b4fa);\n --bpmnkit-focus: #89b4fa;\n --bpmnkit-overlay-bg: rgba(30, 30, 46, 0.92);\n --bpmnkit-overlay-border: rgba(255, 255, 255, 0.1);\n}\n.bpmnkit-canvas-host[data-theme=\"dark\"] .bpmnkit-edge-hover-dot { fill: var(--bpmnkit-accent, #6b9df7); }\n.bpmnkit-canvas-host[data-theme=\"dark\"] .bpmnkit-edge-waypoint-ball { fill: var(--bpmnkit-accent, #6b9df7); }\n";
|
|
5
|
+
/**
|
|
6
|
+
* Injects the canvas stylesheet into `<head>` if not already present.
|
|
7
|
+
* Safe to call multiple times — only one `<style>` tag is ever inserted.
|
|
8
|
+
*/
|
|
9
|
+
export declare function injectStyles(): void;
|
|
10
|
+
//# sourceMappingURL=css.d.ts.map
|