Skip to content
47 changes: 47 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,53 @@ npm run lint # Auto-fix linting issues
npm run lint-check # Check linting without fixing
```

### JSDoc Guidelines

When writing or updating code, follow these JSDoc practices:

**Public API Documentation:**
- **REQUIRED**: All public classes, methods, interfaces, and types must have JSDoc comments
- **REQUIRED**: All new public API elements must include the `@since` tag with the version number
- Infer the next version from `package.json` (current: `0.47.0-post` → next: `0.48.0`)
- Always confirm the target version with the user once per session before using it
- Include description, `@param` for parameters, `@return` for return values
- Document exceptions with `@throws` when applicable
- Use `@example` for complex APIs to show usage patterns
- Mark experimental APIs with `@experimental` tag
- Mark deprecated APIs with `@deprecated` and provide migration guidance

**Internal Code Documentation:**
- **OPTIONAL**: Internal/private code may have JSDoc but it's not required
- Prefer self-documenting code with clear naming over excessive comments
- Add JSDoc for complex internal logic where the "why" isn't obvious
- Use inline comments for non-trivial implementation details

**JSDoc Best Practices:**
- Keep descriptions concise but complete
- Use TypeScript types; avoid redundant type information in JSDoc (rely on `@param {Type}` when TypeScript inference isn't sufficient)
- Link to related types/methods using `{@link ClassName}` or `{@link methodName}`
- Document parameter constraints and valid ranges
- Specify units for numeric parameters (e.g., "duration in milliseconds")

**Example:**
```typescript
/**
* Loads and renders a BPMN diagram from XML.
*
* @param xml The BPMN 2.0 XML content to parse and render
* @param options Optional configuration for rendering behavior
* @returns The loaded model information
* @throws {Error} If the XML is invalid or cannot be parsed
* @since 0.48.0
* @example
* const bpmnVisualization = new BpmnVisualization({ container: 'diagram' });
* bpmnVisualization.load('<definitions>...</definitions>');
*/
public load(xml: string, options?: LoadOptions): LoadResult {
// implementation
}
```

### Documentation
```bash
npm run docs # Generate all documentation
Expand Down
15 changes: 9 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,6 @@ Please check the [__⏩ live environment__](https://cdn.statically.io/gh/process
You will find their basic usage as well as detailed examples showing possible rendering customizations.


## 📂 Repository Structure

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). \
This demo is also used for the PR previews of this repository.


## 🔆 Project Status

`bpmn-visualization` is actively developed and maintained.
Expand Down Expand Up @@ -184,6 +178,15 @@ In the HTML page:
The User documentation (with the feature list & the public API) is available in the [documentation site](https://process-analytics.github.io/bpmn-visualization-js/).


## 🚀 Repository Demo

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.

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`.

For a complete list of configuration options, refer to the [source code](./dev/ts/shared/main.ts).


### ⚒️ More

💡 Want to know more about `bpmn-visualization` usage and extensibility? Have a look at the
Expand Down
48 changes: 48 additions & 0 deletions dev/ts/component/CustomIconPainter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
Copyright 2025 Bonitasoft S.A.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

import type { PaintParameter } from '../../../src/component/mxgraph/shape/render';

import { IconPainter } from '../../../src/component/mxgraph/shape/render';

// 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
// The only difference is the fill color of the icon, which is not set here, to let the theme define it.
export class CustomIconPainter extends IconPainter {
// adapted from https://github.com/primer/octicons/blob/638c6683c96ec4b357576c7897be8f19c933c052/icons/person.svg
// use mxgraph-svg2shape to generate the code from the svg
override paintPersonIcon(paintParameter: PaintParameter): void {
const canvas = this.newBpmnCanvas(paintParameter, { height: 13, width: 12 });
// respect the color in the current theme
canvas.setFillColor(paintParameter.iconStyleConfig.strokeColor);

canvas.begin();
canvas.moveTo(12, 13);
canvas.arcTo(1, 1, 0, 0, 1, 11, 14);
canvas.lineTo(1, 14);
canvas.arcTo(1, 1, 0, 0, 1, 0, 13);
canvas.lineTo(0, 12);
canvas.curveTo(0, 9.37, 4, 8, 4, 8);
canvas.curveTo(4, 8, 4.23, 8, 4, 8);
canvas.curveTo(3.16, 6.38, 3.06, 5.41, 3, 3);
canvas.curveTo(3.17, 0.59, 4.87, 0, 6, 0);
canvas.curveTo(7.13, 0, 8.83, 0.59, 9, 3);
canvas.curveTo(8.94, 5.41, 8.84, 6.38, 8, 8);
canvas.curveTo(8, 8, 12, 9.37, 12, 12);
canvas.lineTo(12, 13);
canvas.close();
canvas.fill();
}
}
11 changes: 9 additions & 2 deletions dev/ts/shared/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import type {
import type { mxCell } from 'mxgraph';

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

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

// Special handling for iconPainter as it requires an instance
if (parameters.get('renderer.iconPainter.use.custom') === 'true') {
rendererOptions.iconPainter = new CustomIconPainter();
logStartup(`Setting renderer option 'iconPainter' with a CustomIconPainter instance`);
}

return rendererOptions;
}

Expand Down
5 changes: 2 additions & 3 deletions src/component/BpmnVisualization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import type { Disposable } from './types';

import { htmlElement } from './helpers/dom-utils';
import { newBpmnRenderer } from './mxgraph/BpmnRenderer';
import GraphConfigurator from './mxgraph/GraphConfigurator';
import { createNewBpmnGraph } from './mxgraph/GraphConfigurator';
import { createNewNavigation } from './navigation';
import { newBpmnParser } from './parser/BpmnParser';
import { createNewBpmnElementsRegistry } from './registry/bpmn-elements-registry';
Expand Down Expand Up @@ -78,8 +78,7 @@ export class BpmnVisualization implements Disposable {
constructor(options: GlobalOptions) {
this.rendererOptions = options?.renderer;
// graph configuration
const configurator = new GraphConfigurator(htmlElement(options?.container));
this.graph = configurator.configure();
this.graph = createNewBpmnGraph(htmlElement(options?.container), this.rendererOptions);
// other configurations
this.navigation = createNewNavigation(this.graph, options?.navigation);
this.bpmnModelRegistry = new BpmnModelRegistry();
Expand Down
52 changes: 48 additions & 4 deletions src/component/mxgraph/BpmnGraph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,22 +14,59 @@ See the License for the specific language governing permissions and
limitations under the License.
*/

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

import { BpmnCellRenderer } from './BpmnCellRenderer';
import { mxgraph } from './initializer';
import { IconPainterProvider } from './shape/render';

/**
* Temporary storage for iconPainter during BpmnGraph construction.
*
* **Problem**: The mxGraph super constructor calls `createCellRenderer()` to set the cellRenderer property
* (via createGraphView at https://github.com/jgraph/mxgraph/blob/v4.2.2/javascript/src/js/view/mxGraph.js#L672).
* However, in JavaScript/TypeScript, instance fields (including constructor parameters declared as fields)
* are initialized AFTER the super() call completes. This means `this.iconPainter` is undefined when
* `createCellRenderer()` is called during super() construction.
*
* **Root cause**: mxGraph uses the **factory method pattern** for initialization. The mxGraph class is in
* charge of its own initialization by calling factory methods (`createCellRenderer()`, `createGraphView()`)
* instead of relying on injected collaborators. This makes it impossible to inject dependencies cleanly.
*
* **Why we can't use other approaches**:
* - Can't use `this` as WeakMap key: TypeScript/JavaScript doesn't allow accessing `this` before `super()` call
* - Can't use `this.container` as key: It's set AFTER createGraphView completes
* (https://github.com/jgraph/mxgraph/blob/v4.2.2/javascript/src/js/view/mxGraph.js#L691)
* - Can't use constructor parameter: Not accessible from `createCellRenderer()` method
*
* **Solution**: Use a module-level temporary variable. This is safe because JavaScript is single-threaded,
* so only one constructor can be executing at a time. The variable is set before `super()`, used during
* `createCellRenderer()`, then immediately cleaned up after construction.
*
* **Future**: If we migrate to maxGraph, this problem won't exist. maxGraph's BaseGraph accepts the
* CellRenderer via constructor options (dependency injection), eliminating the need for this workaround.
* See https://github.com/maxGraph/maxGraph/blob/cb16ce46d5b33df1ea7b2fbc8815c23420d3e658/packages/core/src/view/BaseGraph.ts#L36
*/
let pendingIconPainter: IconPainter | undefined;

export class BpmnGraph extends mxgraph.mxGraph {
/**
* @internal
*/
constructor(container: HTMLElement) {
constructor(container: HTMLElement, iconPainter: IconPainter) {
// Store iconPainter in temporary variable BEFORE super() call
// This makes it available in createCellRenderer() which is called during super() construction
pendingIconPainter = iconPainter;

super(container);

if (this.container) {
// ensure we don't have a select text cursor on label hover, see #294
this.container.style.cursor = 'default';
}

// Clean up the temporary variable now that super() is complete
pendingIconPainter = undefined;
}

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

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

/**
Expand Down
18 changes: 12 additions & 6 deletions src/component/mxgraph/GraphConfigurator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,19 @@ See the License for the specific language governing permissions and
limitations under the License.
*/

import type { RendererOptions } from '../options';

import { BpmnGraph } from './BpmnGraph';
import { registerEdgeMarkers, registerShapes } from './config/register-style-definitions';
import { StyleConfigurator } from './config/StyleConfigurator';
import { IconPainter } from './shape/render';

/**
* @internal
*/
export function createNewBpmnGraph(container: HTMLElement, rendererOptions?: RendererOptions): BpmnGraph {
return new GraphConfigurator(new BpmnGraph(container, rendererOptions?.iconPainter ?? new IconPainter())).configure();
}

/**
* Configure the {@link BpmnGraph} graph that can be used by the lib
Expand All @@ -26,12 +36,8 @@ import { StyleConfigurator } from './config/StyleConfigurator';
* <li>markers
* @internal
*/
export default class GraphConfigurator {
private readonly graph: BpmnGraph;

constructor(container: HTMLElement) {
this.graph = new BpmnGraph(container);
}
export class GraphConfigurator {
constructor(private readonly graph: BpmnGraph) {}

configure(): BpmnGraph {
this.configureGraph();
Expand Down
19 changes: 0 additions & 19 deletions src/component/mxgraph/shape/render/icon-painter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -940,22 +940,3 @@ function paintGearInnerCircle(canvas: BpmnCanvas, arcStartX: number, arcStartY:
canvas.close();
canvas.fillAndStroke();
}

/**
* Hold the instance of {@link IconPainter} used by the BPMN Theme.
*
* **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.
*
* @category BPMN Theme
* @experimental
*/
export class IconPainterProvider {
private static instance = new IconPainter();

static get(): IconPainter {
return this.instance;
}
static set(painter: IconPainter): void {
this.instance = painter;
}
}
2 changes: 1 addition & 1 deletion src/component/mxgraph/shape/render/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,4 @@ limitations under the License.
// export types first, otherwise typedoc doesn't generate the subsequent doc correctly (no category and uses the file header instead of the actual TSDoc)
export type * from './render-types';
export { BpmnCanvas, type BpmnCanvasConfiguration } from './BpmnCanvas';
export { IconPainter, IconPainterProvider, type PaintParameter } from './icon-painter';
export { IconPainter, type PaintParameter } from './icon-painter';
4 changes: 2 additions & 2 deletions src/component/navigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -218,8 +218,8 @@ class ZoomSupport implements Disposable {
this.mouseWheelListeners = [];
}

private createMouseWheelZoomListener(performScaling: boolean) {
return (event: Event, up: boolean) => {
private createMouseWheelZoomListener(performScaling: boolean): MouseWheelListener {
return (event: Event, up: boolean): void => {
if (mxEvent.isConsumed(event) || !(event instanceof MouseEvent)) {
return;
}
Expand Down
30 changes: 30 additions & 0 deletions src/component/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License.
*/

import type { IconPainter } from './mxgraph/shape/render';

/**
* Options to configure the `bpmn-visualization` initialization.
* @category Initialization & Configuration
Expand Down Expand Up @@ -197,6 +199,34 @@ export type ParserOptions = {
* @since 0.35.0
*/
export type RendererOptions = {
/**
* Custom {@link IconPainter} instance to use for rendering BPMN element icons.
* This allows you to customize how icons are rendered on tasks, events, and other BPMN elements.
*
* If not provided, a default {@link IconPainter} instance will be created automatically.
*
* @example
* ```typescript
* import { BpmnVisualization, IconPainter } from 'bpmn-visualization';
*
* class CustomIconPainter extends IconPainter {
* paintPersonIcon(paintParameter) {
* // Custom rendering logic for user task icon
* }
* }
*
* const bpmnVisualization = new BpmnVisualization({
* container: 'bpmn-container',
* renderer: {
* iconPainter: new CustomIconPainter()
* }
* });
* ```
*
* @since 0.48.0
* @default an instance of the default {@link IconPainter} class
*/
iconPainter?: IconPainter;
/**
* If set to `true`, ignore the label bounds configuration defined in the BPMN diagram for all activities.
* This forces the use of default label positioning instead of the bounds specified in the BPMN source.
Expand Down
Loading
Loading