Skip to content

Commit 4d7525d

Browse files
authored
feat!: configure IconPainter via constructor options (#3419)
Remove IconPainterProvider singleton. Users must now pass custom IconPainter through renderer options in BpmnVisualization constructor. BREAKING CHANGE: `IconPainterProvider` removed. Use the `renderer.iconPainter` option instead.
1 parent 43d76cb commit 4d7525d

File tree

13 files changed

+268
-43
lines changed

13 files changed

+268
-43
lines changed

CLAUDE.md

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,53 @@ npm run lint # Auto-fix linting issues
5353
npm run lint-check # Check linting without fixing
5454
```
5555

56+
### JSDoc Guidelines
57+
58+
When writing or updating code, follow these JSDoc practices:
59+
60+
**Public API Documentation:**
61+
- **REQUIRED**: All public classes, methods, interfaces, and types must have JSDoc comments
62+
- **REQUIRED**: All new public API elements must include the `@since` tag with the version number
63+
- Infer the next version from `package.json` (current: `0.47.0-post` → next: `0.48.0`)
64+
- Always confirm the target version with the user once per session before using it
65+
- Include description, `@param` for parameters, `@return` for return values
66+
- Document exceptions with `@throws` when applicable
67+
- Use `@example` for complex APIs to show usage patterns
68+
- Mark experimental APIs with `@experimental` tag
69+
- Mark deprecated APIs with `@deprecated` and provide migration guidance
70+
71+
**Internal Code Documentation:**
72+
- **OPTIONAL**: Internal/private code may have JSDoc but it's not required
73+
- Prefer self-documenting code with clear naming over excessive comments
74+
- Add JSDoc for complex internal logic where the "why" isn't obvious
75+
- Use inline comments for non-trivial implementation details
76+
77+
**JSDoc Best Practices:**
78+
- Keep descriptions concise but complete
79+
- Use TypeScript types; avoid redundant type information in JSDoc (rely on `@param {Type}` when TypeScript inference isn't sufficient)
80+
- Link to related types/methods using `{@link ClassName}` or `{@link methodName}`
81+
- Document parameter constraints and valid ranges
82+
- Specify units for numeric parameters (e.g., "duration in milliseconds")
83+
84+
**Example:**
85+
```typescript
86+
/**
87+
* Loads and renders a BPMN diagram from XML.
88+
*
89+
* @param xml The BPMN 2.0 XML content to parse and render
90+
* @param options Optional configuration for rendering behavior
91+
* @returns The loaded model information
92+
* @throws {Error} If the XML is invalid or cannot be parsed
93+
* @since 0.48.0
94+
* @example
95+
* const bpmnVisualization = new BpmnVisualization({ container: 'diagram' });
96+
* bpmnVisualization.load('<definitions>...</definitions>');
97+
*/
98+
public load(xml: string, options?: LoadOptions): LoadResult {
99+
// implementation
100+
}
101+
```
102+
56103
### Documentation
57104
```bash
58105
npm run docs # Generate all documentation

README.md

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -49,12 +49,6 @@ Please check the [__⏩ live environment__](https://cdn.statically.io/gh/process
4949
You will find their basic usage as well as detailed examples showing possible rendering customizations.
5050

5151

52-
## 📂 Repository Structure
53-
54-
The [dev](./dev) directory contains the source code for the **Load and Navigation demo** showcased on the [example site](https://cdn.statically.io/gh/process-analytics/bpmn-visualization-examples/master/examples/index.html). \
55-
This demo is also used for the PR previews of this repository.
56-
57-
5852
## 🔆 Project Status
5953

6054
`bpmn-visualization` is actively developed and maintained.
@@ -192,6 +186,15 @@ The User documentation (with the feature list & the public API) is available in
192186
For more technical details and how-to, go to the `bpmn-visualization-examples` [repository](https://github.com/process-analytics/bpmn-visualization-examples/).
193187

194188

189+
## 🚀 Repository Demo
190+
191+
This repository includes a **Load and Navigation demo** located in the [dev](./dev) directory, which is also featured on the [example site](https://cdn.statically.io/gh/process-analytics/bpmn-visualization-examples/master/examples/index.html) and used for PR previews.
192+
193+
The demo supports customization through URL query parameters. For instance, you can set a specific theme by passing `style.theme=light-blue`, or enable a custom icon painter with `renderer.iconPainter.use.custom=true`.
194+
195+
For a complete list of configuration options, refer to the [source code](./dev/ts/shared/main.ts).
196+
197+
195198
## 🔧 Contributing
196199

197200
To contribute to `bpmn-visualization`, fork and clone this repository locally and commit your code on a separate branch.
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/*
2+
Copyright 2025 Bonitasoft S.A.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
import type { PaintParameter } from '../../../src/component/mxgraph/shape/render';
18+
19+
import { IconPainter } from '../../../src/component/mxgraph/shape/render';
20+
21+
// Taken from https://github.com/process-analytics/bpmn-visualization-examples/blob/v0.47.0/examples/custom-bpmn-theme/custom-user-task-icon/index.js#L9
22+
// The only difference is the fill color of the icon, which is not set here, to let the theme define it.
23+
export class CustomIconPainter extends IconPainter {
24+
// adapted from https://github.com/primer/octicons/blob/638c6683c96ec4b357576c7897be8f19c933c052/icons/person.svg
25+
// use mxgraph-svg2shape to generate the code from the svg
26+
override paintPersonIcon(paintParameter: PaintParameter): void {
27+
const canvas = this.newBpmnCanvas(paintParameter, { height: 13, width: 12 });
28+
// respect the color of the current theme
29+
canvas.setFillColor(paintParameter.iconStyleConfig.strokeColor);
30+
31+
canvas.begin();
32+
canvas.moveTo(12, 13);
33+
canvas.arcTo(1, 1, 0, 0, 1, 11, 14);
34+
canvas.lineTo(1, 14);
35+
canvas.arcTo(1, 1, 0, 0, 1, 0, 13);
36+
canvas.lineTo(0, 12);
37+
canvas.curveTo(0, 9.37, 4, 8, 4, 8);
38+
canvas.curveTo(4, 8, 4.23, 8, 4, 8);
39+
canvas.curveTo(3.16, 6.38, 3.06, 5.41, 3, 3);
40+
canvas.curveTo(3.17, 0.59, 4.87, 0, 6, 0);
41+
canvas.curveTo(7.13, 0, 8.83, 0.59, 9, 3);
42+
canvas.curveTo(8.94, 5.41, 8.84, 6.38, 8, 8);
43+
canvas.curveTo(8, 8, 12, 9.37, 12, 12);
44+
canvas.lineTo(12, 13);
45+
canvas.close();
46+
canvas.fill();
47+
}
48+
}

dev/ts/shared/main.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import type {
3333
import type { mxCell } from 'mxgraph';
3434

3535
import { FlowKind, ShapeBpmnElementKind } from '../../../src/bpmn-visualization';
36+
import { CustomIconPainter } from '../component/CustomIconPainter';
3637
import { downloadAsPng, downloadAsSvg } from '../component/download';
3738
import { DropFileUserInterface } from '../component/DropFileUserInterface';
3839
import { SvgExporter } from '../component/SvgExporter';
@@ -240,8 +241,8 @@ function getFitOptionsFromParameters(config: BpmnVisualizationDemoConfiguration,
240241
function getRendererOptionsFromParameters(config: BpmnVisualizationDemoConfiguration, parameters: URLSearchParams): RendererOptions {
241242
const rendererOptions: RendererOptions = config.globalOptions.renderer ?? {};
242243

243-
// Mapping between query parameter names and RendererOptions properties
244-
const rendererParameterMappings: Record<string, keyof RendererOptions> = {
244+
// Mapping between query parameter names and RendererOptions boolean properties
245+
const rendererParameterMappings: Record<string, Exclude<keyof RendererOptions, 'iconPainter'>> = {
245246
'renderer.ignore.bpmn.colors': 'ignoreBpmnColors',
246247
'renderer.ignore.label.style': 'ignoreBpmnLabelStyles',
247248
'renderer.ignore.activity.label.bounds': 'ignoreBpmnActivityLabelBounds',
@@ -258,6 +259,12 @@ function getRendererOptionsFromParameters(config: BpmnVisualizationDemoConfigura
258259
}
259260
}
260261

262+
// Special handling for iconPainter as it requires an instance
263+
if (parameters.get('renderer.iconPainter.use.custom') === 'true') {
264+
rendererOptions.iconPainter = new CustomIconPainter();
265+
logStartup(`Setting renderer option 'iconPainter' with a CustomIconPainter instance`);
266+
}
267+
261268
return rendererOptions;
262269
}
263270

src/component/BpmnVisualization.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import type { Disposable } from './types';
2222

2323
import { htmlElement } from './helpers/dom-utils';
2424
import { newBpmnRenderer } from './mxgraph/BpmnRenderer';
25-
import GraphConfigurator from './mxgraph/GraphConfigurator';
25+
import { createNewBpmnGraph } from './mxgraph/GraphConfigurator';
2626
import { createNewNavigation } from './navigation';
2727
import { newBpmnParser } from './parser/BpmnParser';
2828
import { createNewBpmnElementsRegistry } from './registry/bpmn-elements-registry';
@@ -78,8 +78,7 @@ export class BpmnVisualization implements Disposable {
7878
constructor(options: GlobalOptions) {
7979
this.rendererOptions = options?.renderer;
8080
// graph configuration
81-
const configurator = new GraphConfigurator(htmlElement(options?.container));
82-
this.graph = configurator.configure();
81+
this.graph = createNewBpmnGraph(htmlElement(options?.container), this.rendererOptions);
8382
// other configurations
8483
this.navigation = createNewNavigation(this.graph, options?.navigation);
8584
this.bpmnModelRegistry = new BpmnModelRegistry();

src/component/mxgraph/BpmnGraph.ts

Lines changed: 48 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,22 +14,59 @@ See the License for the specific language governing permissions and
1414
limitations under the License.
1515
*/
1616

17+
import type { IconPainter } from './shape/render';
1718
import type { mxCellRenderer, mxCellState, mxGraphView, mxPoint } from 'mxgraph';
1819

1920
import { BpmnCellRenderer } from './BpmnCellRenderer';
2021
import { mxgraph } from './initializer';
21-
import { IconPainterProvider } from './shape/render';
22+
23+
/**
24+
* Temporary storage for iconPainter during BpmnGraph construction.
25+
*
26+
* **Problem**: The mxGraph super constructor calls `createCellRenderer()` to set the cellRenderer property
27+
* (via createGraphView at https://github.com/jgraph/mxgraph/blob/v4.2.2/javascript/src/js/view/mxGraph.js#L672).
28+
* However, in JavaScript/TypeScript, instance fields (including constructor parameters declared as fields)
29+
* are initialized AFTER the super() call completes. This means `this.iconPainter` is undefined when
30+
* `createCellRenderer()` is called during super() construction.
31+
*
32+
* **Root cause**: mxGraph uses the **factory method pattern** for initialization. The mxGraph class is in
33+
* charge of its own initialization by calling factory methods (`createCellRenderer()`, `createGraphView()`)
34+
* instead of relying on injected collaborators. This makes it impossible to inject dependencies cleanly.
35+
*
36+
* **Why we can't use other approaches**:
37+
* - Can't use `this` as WeakMap key: TypeScript/JavaScript doesn't allow accessing `this` before `super()` call
38+
* - Can't use `this.container` as key: It's set AFTER createGraphView completes
39+
* (https://github.com/jgraph/mxgraph/blob/v4.2.2/javascript/src/js/view/mxGraph.js#L691)
40+
* - Can't use constructor parameter: Not accessible from `createCellRenderer()` method
41+
*
42+
* **Solution**: Use a module-level temporary variable. This is safe because JavaScript is single-threaded,
43+
* so only one constructor can be executing at a time. The variable is set before `super()`, used during
44+
* `createCellRenderer()`, then immediately cleaned up after construction.
45+
*
46+
* **Future**: If we migrate to maxGraph, this problem won't exist. maxGraph's BaseGraph accepts the
47+
* CellRenderer via constructor options (dependency injection), eliminating the need for this workaround.
48+
* See https://github.com/maxGraph/maxGraph/blob/cb16ce46d5b33df1ea7b2fbc8815c23420d3e658/packages/core/src/view/BaseGraph.ts#L36
49+
*/
50+
let pendingIconPainter: IconPainter | undefined;
2251

2352
export class BpmnGraph extends mxgraph.mxGraph {
2453
/**
2554
* @internal
2655
*/
27-
constructor(container: HTMLElement) {
56+
constructor(container: HTMLElement, iconPainter: IconPainter) {
57+
// Store iconPainter in temporary variable BEFORE super() call
58+
// This makes it available in createCellRenderer() which is called during super() construction
59+
pendingIconPainter = iconPainter;
60+
2861
super(container);
62+
2963
if (this.container) {
3064
// ensure we don't have a select text cursor on label hover, see #294
3165
this.container.style.cursor = 'default';
3266
}
67+
68+
// Clean up the temporary variable now that super() is complete
69+
pendingIconPainter = undefined;
3370
}
3471

3572
/**
@@ -39,9 +76,16 @@ export class BpmnGraph extends mxgraph.mxGraph {
3976
return new BpmnGraphView(this);
4077
}
4178

79+
/**
80+
* Called by mxGraph super constructor to create the cell renderer.
81+
*
82+
* This method is only called once during construction (by the mxGraph super constructor),
83+
* so we retrieve the iconPainter from the module-level temporary variable.
84+
*
85+
* @internal
86+
*/
4287
override createCellRenderer(): mxCellRenderer {
43-
// in the future, the IconPainter could be configured at library initialization and the provider could be removed
44-
return new BpmnCellRenderer(IconPainterProvider.get());
88+
return new BpmnCellRenderer(pendingIconPainter);
4589
}
4690

4791
/**

src/component/mxgraph/GraphConfigurator.ts

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,19 @@ See the License for the specific language governing permissions and
1414
limitations under the License.
1515
*/
1616

17+
import type { RendererOptions } from '../options';
18+
1719
import { BpmnGraph } from './BpmnGraph';
1820
import { registerEdgeMarkers, registerShapes } from './config/register-style-definitions';
1921
import { StyleConfigurator } from './config/StyleConfigurator';
22+
import { IconPainter } from './shape/render';
23+
24+
/**
25+
* @internal
26+
*/
27+
export function createNewBpmnGraph(container: HTMLElement, rendererOptions?: RendererOptions): BpmnGraph {
28+
return new GraphConfigurator(new BpmnGraph(container, rendererOptions?.iconPainter ?? new IconPainter())).configure();
29+
}
2030

2131
/**
2232
* Configure the {@link BpmnGraph} graph that can be used by the lib
@@ -26,12 +36,8 @@ import { StyleConfigurator } from './config/StyleConfigurator';
2636
* <li>markers
2737
* @internal
2838
*/
29-
export default class GraphConfigurator {
30-
private readonly graph: BpmnGraph;
31-
32-
constructor(container: HTMLElement) {
33-
this.graph = new BpmnGraph(container);
34-
}
39+
export class GraphConfigurator {
40+
constructor(private readonly graph: BpmnGraph) {}
3541

3642
configure(): BpmnGraph {
3743
this.configureGraph();

src/component/mxgraph/shape/render/icon-painter.ts

Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -940,22 +940,3 @@ function paintGearInnerCircle(canvas: BpmnCanvas, arcStartX: number, arcStartY:
940940
canvas.close();
941941
canvas.fillAndStroke();
942942
}
943-
944-
/**
945-
* Hold the instance of {@link IconPainter} used by the BPMN Theme.
946-
*
947-
* **WARN**: You may use it to customize the BPMN Theme as suggested in the examples. But be aware that the way the default BPMN theme can be modified is subject to change.
948-
*
949-
* @category BPMN Theme
950-
* @experimental
951-
*/
952-
export class IconPainterProvider {
953-
private static instance = new IconPainter();
954-
955-
static get(): IconPainter {
956-
return this.instance;
957-
}
958-
static set(painter: IconPainter): void {
959-
this.instance = painter;
960-
}
961-
}

src/component/mxgraph/shape/render/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,4 @@ limitations under the License.
1717
// export types first, otherwise typedoc doesn't generate the subsequent doc correctly (no category and uses the file header instead of the actual TSDoc)
1818
export type * from './render-types';
1919
export { BpmnCanvas, type BpmnCanvasConfiguration } from './BpmnCanvas';
20-
export { IconPainter, IconPainterProvider, type PaintParameter } from './icon-painter';
20+
export { IconPainter, type PaintParameter } from './icon-painter';

src/component/navigation.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -218,8 +218,8 @@ class ZoomSupport implements Disposable {
218218
this.mouseWheelListeners = [];
219219
}
220220

221-
private createMouseWheelZoomListener(performScaling: boolean) {
222-
return (event: Event, up: boolean) => {
221+
private createMouseWheelZoomListener(performScaling: boolean): MouseWheelListener {
222+
return (event: Event, up: boolean): void => {
223223
if (mxEvent.isConsumed(event) || !(event instanceof MouseEvent)) {
224224
return;
225225
}

0 commit comments

Comments
 (0)