├── .editorconfig
├── .github
├── dependabot.yml
├── release.yml
└── workflows
│ ├── build.yaml
│ └── publish.yaml
├── .gitignore
├── .idea
├── $CACHE_FILE$
├── .gitignore
├── camunda-web-modeler.iml
├── codeStyles
│ ├── Project.xml
│ └── codeStyleConfig.xml
├── inspectionProfiles
│ └── Project_Default.xml
├── jsLibraryMappings.xml
├── misc.xml
├── modules.xml
├── prettier.xml
├── saveactions_settings.xml
└── vcs.xml
├── .nvmrc
├── .prettierignore
├── .prettierrc
├── .yarnrc.yml
├── LICENSE
├── README.md
├── eslint.config.mjs
├── index.ts
├── package.json
├── rollup.config.mjs
├── src
├── BpmnModeler.tsx
├── DmnModeler.tsx
├── bpmnio
│ ├── GlobalEventListenerUtil.ts
│ ├── bpmn
│ │ └── CustomBpmnJsModeler.ts
│ ├── dmn
│ │ └── CustomDmnJsModeler.ts
│ └── index.ts
├── components
│ ├── SvgIcon.tsx
│ └── ToggleGroup.tsx
├── editor
│ ├── BpmnEditor.tsx
│ ├── DmnEditor.tsx
│ └── XmlEditor.tsx
├── events
│ ├── Events.ts
│ ├── bpmnio
│ │ └── BpmnIoEvents.ts
│ ├── index.ts
│ └── modeler
│ │ ├── ContentSavedEvent.ts
│ │ ├── DmnViewsChangedEvent.ts
│ │ ├── NotificationEvent.ts
│ │ ├── PropertiesPanelResizedEvent.ts
│ │ └── UIUpdateRequiredEvent.ts
├── index.ts
└── types
│ ├── bpmn-io.d.ts
│ ├── bpmn-js-element-templates.ts
│ ├── bpmn-js-properties-panel.d.ts
│ ├── bpmn-js.d.ts
│ ├── diagram-js-origin.d.ts
│ ├── dmn-js-properties-panel.d.ts
│ └── dmn-js.d.ts
├── static
└── screenshot.jpg
├── tsconfig.index.json
├── tsconfig.json
└── yarn.lock
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | charset = utf-8
5 | indent_style = space
6 | indent_size = 4
7 | end_of_line = lf
8 | insert_final_newline = true
9 | max_line_length = 89
10 | trim_trailing_whitespace = false
11 |
12 | # intellij ( ij_programming-language_setting )
13 | ij_any_line_comment_add_space_on_reformat = true
14 | # spaces
15 | ij_any_spaces_within_method_parentheses = true
16 | ij_any_spaces_within_method_call_parentheses = true
17 | ij_any_spaces_within_if_parentheses = true
18 | ij_any_spaces_within_for_parentheses = true
19 | ij_any_spaces_within_while_parentheses = true
20 | ij_any_spaces_within_switch_parentheses = true
21 | ij_any_spaces_within_catch_parentheses = true
22 | # braces
23 | ij_any_class_brace_style = next_line
24 | ij_any_method_brace_style = next_line
25 |
26 | [*{json,yml,yaml}]
27 | indent_size = 2
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | # Fetch and update latest `npm` packages
4 | - package-ecosystem: npm
5 | directory: "/"
6 | target-branch: "develop"
7 | labels:
8 | - "Technical Debt"
9 | schedule:
10 | interval: daily
11 | time: "05:00"
12 | commit-message:
13 | prefix: fix
14 | prefix-development: chore
15 | include: scope
16 | # Fetch and update latest `github-actions` packages
17 | - package-ecosystem: github-actions
18 | directory: "/"
19 | schedule:
20 | interval: weekly
21 | time: "05:00"
22 | target-branch: "develop"
23 | labels:
24 | - "Technical Debt"
25 | commit-message:
26 | prefix: fix
27 | prefix-development: chore
28 | include: scope
29 |
--------------------------------------------------------------------------------
/.github/release.yml:
--------------------------------------------------------------------------------
1 | changelog:
2 | categories:
3 | - title: 🎉 New Features
4 | labels:
5 | - feature
6 | - title: 🐞 Bug Fixes
7 | labels:
8 | - bug
9 | - title: 🔨 Refactoring
10 | labels:
11 | - refactoring
12 | - title: 📔 Documentation
13 | labels:
14 | - docs
15 | - title: 🛠️ Misc
16 | labels:
17 | - Technical Debt
18 | - chore
--------------------------------------------------------------------------------
/.github/workflows/build.yaml:
--------------------------------------------------------------------------------
1 | name: Build
2 |
3 | on:
4 | push:
5 | branches-ignore: [ develop ]
6 | pull_request:
7 | branches: [ develop ]
8 |
9 | jobs:
10 | build:
11 | runs-on: ubuntu-latest
12 | steps:
13 | - name: Checkout sources
14 | uses: actions/checkout@v4
15 | - name: Enable corepack
16 | run: corepack enable
17 | - name: Setup NodeJS 20.x
18 | uses: actions/setup-node@v4
19 | with:
20 | node-version: 20.x
21 | cache: 'yarn'
22 | - name: Install dependencies
23 | run: yarn
24 | - name: Build application
25 | run: yarn build
26 |
--------------------------------------------------------------------------------
/.github/workflows/publish.yaml:
--------------------------------------------------------------------------------
1 | name: Publish
2 |
3 | on:
4 | workflow_dispatch:
5 | inputs:
6 | release-tag:
7 | description: 'Release tag'
8 | required: false
9 | default: 'release/v0.0.0'
10 |
11 | jobs:
12 | release:
13 | runs-on: ubuntu-latest
14 | steps:
15 | - name: Checkout sources
16 | uses: actions/checkout@v4
17 | - name: Enable corepack
18 | run: corepack enable
19 | - name: Setup NodeJS 20.x
20 | uses: actions/setup-node@v4
21 | with:
22 | node-version: 20.x
23 | registry-url: 'https://registry.npmjs.org'
24 | cache: 'yarn'
25 | - name: Install dependencies
26 | run: yarn
27 | - name: Build application
28 | run: yarn build
29 | - name: Setup .yarnrc.yml
30 | run: yarn config set npmAuthToken $NPM_AUTH_TOKEN
31 | env:
32 | NPM_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_SECRET }}
33 | - name: Publish package
34 | run: yarn npm publish --access public
35 | env:
36 | NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_SECRET }}
37 |
38 | github-release:
39 | if: github.event.inputs.release-tag
40 | runs-on: ubuntu-latest
41 | needs:
42 | - release
43 | steps:
44 | - name: PREP / Checkout code
45 | uses: actions/checkout@v4
46 | - name: GIT / Create tag
47 | uses: actions/github-script@v7
48 | with:
49 | script: |
50 | github.rest.git.createRef({
51 | owner: context.repo.owner,
52 | repo: context.repo.repo,
53 | ref: 'refs/tags/${{ github.event.inputs.release-tag }}',
54 | sha: context.sha
55 | })
56 | - name: GIT / Create GitHub Release
57 | id: create_release
58 | uses: softprops/action-gh-release@v2
59 | with:
60 | tag_name: ${{ github.event.inputs.release-tag }}
61 | draft: false
62 | prerelease: false
63 | generate_release_notes: true
64 |
65 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | # Created by https://www.gitignore.io/api/git,node,react,linux,macos,windows,typings,intellij,webstorm,visualstudiocode
3 | # Edit at https://www.gitignore.io/?templates=git,node,react,linux,macos,windows,typings,intellij,webstorm,visualstudiocode
4 |
5 | ### Git ###
6 | # Created by git for backups. To disable backups in Git:
7 | # $ git config --global mergetool.keepBackup false
8 | *.orig
9 |
10 | # Created by git when using merge tools for conflicts
11 | *.BACKUP.*
12 | *.BASE.*
13 | *.LOCAL.*
14 | *.REMOTE.*
15 | *_BACKUP_*.txt
16 | *_BASE_*.txt
17 | *_LOCAL_*.txt
18 | *_REMOTE_*.txt
19 |
20 | ### Intellij ###
21 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm
22 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
23 |
24 | # User-specific stuff
25 | .idea/**/workspace.xml
26 | .idea/**/tasks.xml
27 | .idea/**/usage.statistics.xml
28 | .idea/**/dictionaries
29 | .idea/**/shelf
30 |
31 | # Generated files
32 | .idea/**/contentModel.xml
33 |
34 | # Sensitive or high-churn files
35 | .idea/**/dataSources/
36 | .idea/**/dataSources.ids
37 | .idea/**/dataSources.local.xml
38 | .idea/**/sqlDataSources.xml
39 | .idea/**/dynamic.xml
40 | .idea/**/uiDesigner.xml
41 | .idea/**/dbnavigator.xml
42 |
43 | # Gradle
44 | .idea/**/gradle.xml
45 | .idea/**/libraries
46 |
47 | # Gradle and Maven with auto-import
48 | # When using Gradle or Maven with auto-import, you should exclude module files,
49 | # since they will be recreated, and may cause churn. Uncomment if using
50 | # auto-import.
51 | # .idea/modules.xml
52 | # .idea/*.iml
53 | # .idea/modules
54 | # *.iml
55 | # *.ipr
56 |
57 | # CMake
58 | cmake-build-*/
59 |
60 | # Mongo Explorer plugin
61 | .idea/**/mongoSettings.xml
62 |
63 | # File-based project format
64 | *.iws
65 |
66 | # IntelliJ
67 | out/
68 |
69 | # mpeltonen/sbt-idea plugin
70 | .idea_modules/
71 |
72 | # JIRA plugin
73 | atlassian-ide-plugin.xml
74 |
75 | # Cursive Clojure plugin
76 | .idea/replstate.xml
77 |
78 | # Crashlytics plugin (for Android Studio and IntelliJ)
79 | com_crashlytics_export_strings.xml
80 | crashlytics.properties
81 | crashlytics-build.properties
82 | fabric.properties
83 |
84 | # Editor-based Rest Client
85 | .idea/httpRequests
86 |
87 | # Android studio 3.1+ serialized cache file
88 | .idea/caches/build_file_checksums.ser
89 |
90 | ### Intellij Patch ###
91 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721
92 |
93 | # *.iml
94 | # modules.xml
95 | # .idea/misc.xml
96 | # *.ipr
97 |
98 | # Sonarlint plugin
99 | .idea/**/sonarlint/
100 |
101 | # SonarQube Plugin
102 | .idea/**/sonarIssues.xml
103 |
104 | # Markdown Navigator plugin
105 | .idea/**/markdown-navigator.xml
106 | .idea/**/markdown-navigator/
107 |
108 | ### Linux ###
109 | *~
110 |
111 | # temporary files which can be created if a process still has a handle open of a deleted file
112 | .fuse_hidden*
113 |
114 | # KDE directory preferences
115 | .directory
116 |
117 | # Linux trash folder which might appear on any partition or disk
118 | .Trash-*
119 |
120 | # .nfs files are created when an open file is removed but is still being accessed
121 | .nfs*
122 |
123 | ### macOS ###
124 | # General
125 | .DS_Store
126 | .AppleDouble
127 | .LSOverride
128 |
129 | # Icon must end with two \r
130 | Icon
131 |
132 | # Thumbnails
133 | ._*
134 |
135 | # Files that might appear in the root of a volume
136 | .DocumentRevisions-V100
137 | .fseventsd
138 | .Spotlight-V100
139 | .TemporaryItems
140 | .Trashes
141 | .VolumeIcon.icns
142 | .com.apple.timemachine.donotpresent
143 |
144 | # Directories potentially created on remote AFP share
145 | .AppleDB
146 | .AppleDesktop
147 | Network Trash Folder
148 | Temporary Items
149 | .apdisk
150 |
151 | ### Node ###
152 | # Logs
153 | logs
154 | *.log
155 | npm-debug.log*
156 | lerna-debug.log*
157 |
158 | # Diagnostic reports (https://nodejs.org/api/report.html)
159 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
160 |
161 | # Runtime data
162 | pids
163 | *.pid
164 | *.seed
165 | *.pid.lock
166 |
167 | # Directory for instrumented libs generated by jscoverage/JSCover
168 | lib-cov
169 |
170 | # Coverage directory used by tools like istanbul
171 | coverage
172 | *.lcov
173 |
174 | # nyc test coverage
175 | .nyc_output
176 |
177 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
178 | .grunt
179 |
180 | # Bower dependency directory (https://bower.io/)
181 | bower_components
182 |
183 | # node-waf configuration
184 | .lock-wscript
185 |
186 | # Compiled binary addons (https://nodejs.org/api/addons.html)
187 | build/Release
188 |
189 | # Dependency directories
190 | node_modules/
191 | jspm_packages/
192 |
193 | # TypeScript v1 declaration files
194 | typings/
195 |
196 | # TypeScript cache
197 | *.tsbuildinfo
198 |
199 | # Optional npm cache directory
200 | .npm
201 |
202 | # Optional eslint cache
203 | .eslintcache
204 |
205 | # Optional REPL history
206 | .node_repl_history
207 |
208 | # Output of 'npm pack'
209 | *.tgz
210 |
211 | # dotenv environment variables file
212 | .env
213 | .env.test
214 |
215 | # parcel-bundler cache (https://parceljs.org/)
216 | .cache
217 |
218 | # next.js build output
219 | .next
220 |
221 | # nuxt.js build output
222 | .nuxt
223 |
224 | # rollup.js default build output
225 | dist/
226 |
227 | # Uncomment the public line if your project uses Gatsby
228 | # https://nextjs.org/blog/next-9-1#public-directory-support
229 | # https://create-react-app.dev/docs/using-the-public-folder/#docsNav
230 | # public
231 |
232 | # Storybook build outputs
233 | .out
234 | .storybook-out
235 |
236 | # vuepress build output
237 | .vuepress/dist
238 |
239 | # Serverless directories
240 | .serverless/
241 |
242 | # FuseBox cache
243 | .fusebox/
244 |
245 | # DynamoDB Local files
246 | .dynamodb/
247 |
248 | # Temporary folders
249 | tmp/
250 | temp/
251 |
252 | ### react ###
253 | .DS_*
254 | **/*.backup.*
255 | **/*.back.*
256 |
257 | node_modules
258 |
259 | *.sublime*
260 |
261 | psd
262 | thumb
263 | sketch
264 |
265 | ### Typings ###
266 | ## Ignore downloaded typings
267 | typings
268 |
269 | ### VisualStudioCode ###
270 | .vscode/*
271 | !.vscode/settings.json
272 | !.vscode/tasks.json
273 | !.vscode/launch.json
274 | !.vscode/extensions.json
275 |
276 | ### VisualStudioCode Patch ###
277 | # Ignore all local history of files
278 | .history
279 |
280 | ### WebStorm ###
281 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm
282 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
283 |
284 | # User-specific stuff
285 |
286 | # Generated files
287 |
288 | # Sensitive or high-churn files
289 |
290 | # Gradle
291 |
292 | # Gradle and Maven with auto-import
293 | # When using Gradle or Maven with auto-import, you should exclude module files,
294 | # since they will be recreated, and may cause churn. Uncomment if using
295 | # auto-import.
296 | # .idea/modules.xml
297 | # .idea/*.iml
298 | # .idea/modules
299 | # *.iml
300 | # *.ipr
301 |
302 | # CMake
303 |
304 | # Mongo Explorer plugin
305 |
306 | # File-based project format
307 |
308 | # IntelliJ
309 |
310 | # mpeltonen/sbt-idea plugin
311 |
312 | # JIRA plugin
313 |
314 | # Cursive Clojure plugin
315 |
316 | # Crashlytics plugin (for Android Studio and IntelliJ)
317 |
318 | # Editor-based Rest Client
319 |
320 | # Android studio 3.1+ serialized cache file
321 |
322 | ### WebStorm Patch ###
323 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721
324 |
325 | # *.iml
326 | # modules.xml
327 | # .idea/misc.xml
328 | # *.ipr
329 |
330 | # Sonarlint plugin
331 |
332 | # SonarQube Plugin
333 |
334 | # Markdown Navigator plugin
335 |
336 | ### Windows ###
337 | # Windows thumbnail cache files
338 | Thumbs.db
339 | Thumbs.db:encryptable
340 | ehthumbs.db
341 | ehthumbs_vista.db
342 |
343 | # Dump file
344 | *.stackdump
345 |
346 | # Folder config file
347 | [Dd]esktop.ini
348 |
349 | # Recycle Bin used on file shares
350 | $RECYCLE.BIN/
351 |
352 | # Windows Installer files
353 | *.cab
354 | *.msi
355 | *.msix
356 | *.msm
357 | *.msp
358 |
359 | # Windows shortcuts
360 | *.lnk
361 |
362 | # End of https://www.gitignore.io/api/git,node,react,linux,macos,windows,typings,intellij,webstorm,visualstudiocode
363 |
364 | build/
365 | index.js
366 | index.d.ts
367 |
368 | # Yarn (without zero-installs)
369 | .pnp.*
370 | .yarn/*
371 | !.yarn/patches
372 | !.yarn/plugins
373 | !.yarn/releases
374 | !.yarn/sdks
375 | !.yarn/versions
--------------------------------------------------------------------------------
/.idea/$CACHE_FILE$:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 | # Editor-based HTTP Client requests
5 | /httpRequests/
6 |
--------------------------------------------------------------------------------
/.idea/camunda-web-modeler.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/.idea/codeStyles/Project.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
--------------------------------------------------------------------------------
/.idea/codeStyles/codeStyleConfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/Project_Default.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/.idea/jsLibraryMappings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.idea/prettier.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/.idea/saveactions_settings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.nvmrc:
--------------------------------------------------------------------------------
1 | v20
2 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | # Add files here to ignore them from prettier formatting
2 |
3 | /dist
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "printWidth": 89,
3 | "singleQuote": false,
4 | "trailingComma": "all",
5 | "tabWidth": 4,
6 | "arrowParens": "avoid"
7 | }
--------------------------------------------------------------------------------
/.yarnrc.yml:
--------------------------------------------------------------------------------
1 | nodeLinker: node-modules
2 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 |
2 | Apache License
3 | Version 2.0, January 2004
4 | http://www.apache.org/licenses/
5 |
6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7 |
8 | 1. Definitions.
9 |
10 | "License" shall mean the terms and conditions for use, reproduction,
11 | and distribution as defined by Sections 1 through 9 of this document.
12 |
13 | "Licensor" shall mean the copyright owner or entity authorized by
14 | the copyright owner that is granting the License.
15 |
16 | "Legal Entity" shall mean the union of the acting entity and all
17 | other entities that control, are controlled by, or are under common
18 | control with that entity. For the purposes of this definition,
19 | "control" means (i) the power, direct or indirect, to cause the
20 | direction or management of such entity, whether by contract or
21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
22 | outstanding shares, or (iii) beneficial ownership of such entity.
23 |
24 | "You" (or "Your") shall mean an individual or Legal Entity
25 | exercising permissions granted by this License.
26 |
27 | "Source" form shall mean the preferred form for making modifications,
28 | including but not limited to software source code, documentation
29 | source, and configuration files.
30 |
31 | "Object" form shall mean any form resulting from mechanical
32 | transformation or translation of a Source form, including but
33 | not limited to compiled object code, generated documentation,
34 | and conversions to other media types.
35 |
36 | "Work" shall mean the work of authorship, whether in Source or
37 | Object form, made available under the License, as indicated by a
38 | copyright notice that is included in or attached to the work
39 | (an example is provided in the Appendix below).
40 |
41 | "Derivative Works" shall mean any work, whether in Source or Object
42 | form, that is based on (or derived from) the Work and for which the
43 | editorial revisions, annotations, elaborations, or other modifications
44 | represent, as a whole, an original work of authorship. For the purposes
45 | of this License, Derivative Works shall not include works that remain
46 | separable from, or merely link (or bind by name) to the interfaces of,
47 | the Work and Derivative Works thereof.
48 |
49 | "Contribution" shall mean any work of authorship, including
50 | the original version of the Work and any modifications or additions
51 | to that Work or Derivative Works thereof, that is intentionally
52 | submitted to Licensor for inclusion in the Work by the copyright owner
53 | or by an individual or Legal Entity authorized to submit on behalf of
54 | the copyright owner. For the purposes of this definition, "submitted"
55 | means any form of electronic, verbal, or written communication sent
56 | to the Licensor or its representatives, including but not limited to
57 | communication on electronic mailing lists, source code control systems,
58 | and issue tracking systems that are managed by, or on behalf of, the
59 | Licensor for the purpose of discussing and improving the Work, but
60 | excluding communication that is conspicuously marked or otherwise
61 | designated in writing by the copyright owner as "Not a Contribution."
62 |
63 | "Contributor" shall mean Licensor and any individual or Legal Entity
64 | on behalf of whom a Contribution has been received by Licensor and
65 | subsequently incorporated within the Work.
66 |
67 | 2. Grant of Copyright License. Subject to the terms and conditions of
68 | this License, each Contributor hereby grants to You a perpetual,
69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70 | copyright license to reproduce, prepare Derivative Works of,
71 | publicly display, publicly perform, sublicense, and distribute the
72 | Work and such Derivative Works in Source or Object form.
73 |
74 | 3. Grant of Patent License. Subject to the terms and conditions of
75 | this License, each Contributor hereby grants to You a perpetual,
76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77 | (except as stated in this section) patent license to make, have made,
78 | use, offer to sell, sell, import, and otherwise transfer the Work,
79 | where such license applies only to those patent claims licensable
80 | by such Contributor that are necessarily infringed by their
81 | Contribution(s) alone or by combination of their Contribution(s)
82 | with the Work to which such Contribution(s) was submitted. If You
83 | institute patent litigation against any entity (including a
84 | cross-claim or counterclaim in a lawsuit) alleging that the Work
85 | or a Contribution incorporated within the Work constitutes direct
86 | or contributory patent infringement, then any patent licenses
87 | granted to You under this License for that Work shall terminate
88 | as of the date such litigation is filed.
89 |
90 | 4. Redistribution. You may reproduce and distribute copies of the
91 | Work or Derivative Works thereof in any medium, with or without
92 | modifications, and in Source or Object form, provided that You
93 | meet the following conditions:
94 |
95 | (a) You must give any other recipients of the Work or
96 | Derivative Works a copy of this License; and
97 |
98 | (b) You must cause any modified files to carry prominent notices
99 | stating that You changed the files; and
100 |
101 | (c) You must retain, in the Source form of any Derivative Works
102 | that You distribute, all copyright, patent, trademark, and
103 | attribution notices from the Source form of the Work,
104 | excluding those notices that do not pertain to any part of
105 | the Derivative Works; and
106 |
107 | (d) If the Work includes a "NOTICE" text file as part of its
108 | distribution, then any Derivative Works that You distribute must
109 | include a readable copy of the attribution notices contained
110 | within such NOTICE file, excluding those notices that do not
111 | pertain to any part of the Derivative Works, in at least one
112 | of the following places: within a NOTICE text file distributed
113 | as part of the Derivative Works; within the Source form or
114 | documentation, if provided along with the Derivative Works; or,
115 | within a display generated by the Derivative Works, if and
116 | wherever such third-party notices normally appear. The contents
117 | of the NOTICE file are for informational purposes only and
118 | do not modify the License. You may add Your own attribution
119 | notices within Derivative Works that You distribute, alongside
120 | or as an addendum to the NOTICE text from the Work, provided
121 | that such additional attribution notices cannot be construed
122 | as modifying the License.
123 |
124 | You may add Your own copyright statement to Your modifications and
125 | may provide additional or different license terms and conditions
126 | for use, reproduction, or distribution of Your modifications, or
127 | for any such Derivative Works as a whole, provided Your use,
128 | reproduction, and distribution of the Work otherwise complies with
129 | the conditions stated in this License.
130 |
131 | 5. Submission of Contributions. Unless You explicitly state otherwise,
132 | any Contribution intentionally submitted for inclusion in the Work
133 | by You to the Licensor shall be under the terms and conditions of
134 | this License, without any additional terms or conditions.
135 | Notwithstanding the above, nothing herein shall supersede or modify
136 | the terms of any separate license agreement you may have executed
137 | with Licensor regarding such Contributions.
138 |
139 | 6. Trademarks. This License does not grant permission to use the trade
140 | names, trademarks, service marks, or product names of the Licensor,
141 | except as required for reasonable and customary use in describing the
142 | origin of the Work and reproducing the content of the NOTICE file.
143 |
144 | 7. Disclaimer of Warranty. Unless required by applicable law or
145 | agreed to in writing, Licensor provides the Work (and each
146 | Contributor provides its Contributions) on an "AS IS" BASIS,
147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148 | implied, including, without limitation, any warranties or conditions
149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150 | PARTICULAR PURPOSE. You are solely responsible for determining the
151 | appropriateness of using or redistributing the Work and assume any
152 | risks associated with Your exercise of permissions under this License.
153 |
154 | 8. Limitation of Liability. In no event and under no legal theory,
155 | whether in tort (including negligence), contract, or otherwise,
156 | unless required by applicable law (such as deliberate and grossly
157 | negligent acts) or agreed to in writing, shall any Contributor be
158 | liable to You for damages, including any direct, indirect, special,
159 | incidental, or consequential damages of any character arising as a
160 | result of this License or out of the use or inability to use the
161 | Work (including but not limited to damages for loss of goodwill,
162 | work stoppage, computer failure or malfunction, or any and all
163 | other commercial damages or losses), even if such Contributor
164 | has been advised of the possibility of such damages.
165 |
166 | 9. Accepting Warranty or Additional Liability. While redistributing
167 | the Work or Derivative Works thereof, You may choose to offer,
168 | and charge a fee for, acceptance of support, warranty, indemnity,
169 | or other liability obligations and/or rights consistent with this
170 | License. However, in accepting such obligations, You may act only
171 | on Your own behalf and on Your sole responsibility, not on behalf
172 | of any other Contributor, and only if You agree to indemnify,
173 | defend, and hold each Contributor harmless for any liability
174 | incurred by, or claims asserted against, such Contributor by reason
175 | of your accepting any such warranty or additional liability.
176 |
177 | END OF TERMS AND CONDITIONS
178 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Camunda Web Modeler
2 |
3 | [](https://www.npmjs.com/package/@miragon/camunda-web-modeler)
4 | 
5 | 
6 |
7 | 
8 |
9 | This is a React component based on [bpmn.io](https://bpmn.io) that allows you to use a fully functional modeler for BPMN
10 | and DMN in your browser application. It has lots of configuration options and offers these features:
11 |
12 | - Full support for BPMN and DMN
13 | - Embedded XML editor
14 | - Easily import element templates
15 | - Full support for using bpmn.io plugins
16 | - Access to all bpmn.io and additional events to integrate it more easily into your application
17 | - Exposes the complete bpmn.io API
18 | - Includes type definitions for many of the undocumented features and endpoints of bpmn.io
19 | - TypeScript support
20 |
21 | # Usage
22 |
23 | > [!IMPORTANT]
24 | > There is a known issue with the latest version of `min-dash`
25 | > (read about
26 | > it [here](https://forum.bpmn.io/t/camunda-properties-provider-not-working-properly-in-react-app/10660/10)).
27 | > To fix this, you can force the version of `min-dash` to below `4.2` in your `package.json` or configure your bundler
28 | > to also include `.cjs` and/or `.mjs` files.
29 | >
30 | > | npm | yarn |
31 | > |-----------------------------------------------------------------------|-------------------------------------------------------------------------|
32 | > |
"overrides": { "min-dash": "4.1.1" } | "resolutions": { "min-dash": "4.1.1" } |
33 |
34 | ## Getting Started
35 |
36 | 1. Add this dependency to your application:
37 |
38 | ```
39 |
40 | yarn add @miragon/camunda-web-modeler
41 |
42 | ```
43 |
44 | 2. Include it in your application:
45 |
46 | ```typescript
47 | import {
48 | BpmnModeler,
49 | CustomBpmnJsModeler,
50 | Event,
51 | isContentSavedEvent
52 | } from "@miragon/camunda-web-modeler";
53 | import React, { useCallback, useMemo, useRef, useState } from 'react';
54 | import './App.css';
55 |
56 | const App: React.FC = () => {
57 | const modelerRef = useRef();
58 |
59 | const [xml, setXml] = useState(BPMN);
60 |
61 | const onEvent = useCallback(async (event: Event) => {
62 | if (isContentSavedEvent(event)) {
63 | setXml(event.data.xml);
64 | return;
65 | }
66 | }, []);
67 |
68 | /**
69 | * ====
70 | * CAUTION: Using useMemo() is important to prevent additional render cycles!
71 | * ====
72 | */
73 |
74 | const modelerTabOptions = useMemo(() => ({
75 | modelerOptions: {
76 | refs: [modelerRef]
77 | }
78 | }), []);
79 |
80 | return (
81 |
86 |
91 | < /div>
92 | )
93 | ;
94 | }
95 |
96 | export default App;
97 |
98 | const BPMN = /* ... */;
99 | ```
100 |
101 | 3. Include your BPMN in the last line and run the application!
102 |
103 | ## Full example
104 |
105 | To see all options available, you can use this example. Remember that it's important to wrap all options and callbacks
106 | that are passed into the component using `useMemo()` and `useCallback()`. Else you will have lots of additional render
107 | cycles that can lead to bugs that are difficult to debug.
108 |
109 | Using the `bpmnJsOptions`, you can pass any options that you would normally pass into bpmn.io. The component will merge
110 | these with its own options and use it to create the modeler instance.
111 |
112 | ```typescript
113 | import {
114 | BpmnModeler,
115 | ContentSavedReason,
116 | CustomBpmnJsModeler,
117 | Event,
118 | isBpmnIoEvent,
119 | isContentSavedEvent,
120 | isNotificationEvent,
121 | isPropertiesPanelResizedEvent,
122 | isUIUpdateRequiredEvent
123 | } from "@miragon/camunda-web-modeler-test";
124 | import React, { useCallback, useMemo, useRef, useState } from 'react';
125 | import './App.css';
126 |
127 | const App: React.FC = () => {
128 | const modelerRef = useRef
();
129 |
130 | const [xml, setXml] = useState(BPMN);
131 |
132 | const onXmlChanged = useCallback((
133 | newXml: string,
134 | newSvg: string | undefined,
135 | reason: ContentSavedReason
136 | ) => {
137 | console.log(`Model has been changed because of ${reason}`);
138 | // Do whatever you want here, save the XML and SVG in the backend etc.
139 | setXml(newXml);
140 | }, []);
141 |
142 | const onSaveClicked = useCallback(async () => {
143 | if (!modelerRef.current) {
144 | // Should actually never happen, but required for type safety
145 | return;
146 | }
147 |
148 | console.log("Saving model...");
149 | const result = await modelerRef.current.save();
150 | console.log("Saved model!", result.xml, result.svg);
151 | }, []);
152 |
153 | const onEvent = useCallback(async (event: Event) => {
154 | if (isContentSavedEvent(event)) {
155 | // Content has been saved, e.g. because user edited the model or because he switched
156 | // from BPMN to XML.
157 | onXmlChanged(event.data.xml, event.data.svg, event.data.reason);
158 | return;
159 | }
160 |
161 | if (isNotificationEvent(event)) {
162 | // There's a notification the user is supposed to see, e.g. the model could not be
163 | // imported because it was invalid.
164 | return;
165 | }
166 |
167 | if (isUIUpdateRequiredEvent(event)) {
168 | // Something in the modeler has changed and the UI (e.g. menu) should be updated.
169 | // This happens when the user selects an element, for example.
170 | return;
171 | }
172 |
173 | if (isPropertiesPanelResizedEvent(event)) {
174 | // The user has resized the properties panel. You can save this value e.g. in local
175 | // storage to restore it on next load and pass it as initializing option.
176 | console.log(`Properties panel has been resized to ${event.data.width}`);
177 | return;
178 | }
179 |
180 | if (isBpmnIoEvent(event)) {
181 | // Just a regular bpmn-js event - actually lots of them
182 | return;
183 | }
184 |
185 | // eslint-disable-next-line no-console
186 | console.log("Unhandled event received", event);
187 | }, [onXmlChanged]);
188 |
189 | /**
190 | * ====
191 | * CAUTION: Using useMemo() is important to prevent additional render cycles!
192 | * ====
193 | */
194 |
195 | const xmlTabOptions = useMemo(() => ({
196 | className: undefined,
197 | disabled: undefined,
198 | monacoOptions: undefined
199 | }), []);
200 |
201 | const propertiesPanelOptions = useMemo(() => ({
202 | className: undefined,
203 | containerId: undefined,
204 | container: undefined,
205 | elementTemplates: undefined,
206 | hidden: undefined,
207 | size: {
208 | max: undefined,
209 | min: undefined,
210 | initial: undefined
211 | }
212 | }), []);
213 |
214 | const modelerOptions = useMemo(() => ({
215 | className: undefined,
216 | refs: [modelerRef],
217 | container: undefined,
218 | containerId: undefined,
219 | size: {
220 | max: undefined,
221 | min: undefined,
222 | initial: undefined
223 | }
224 | }), []);
225 |
226 | const bpmnJsOptions = useMemo(() => undefined, []);
227 |
228 | const modelerTabOptions = useMemo(() => ({
229 | className: undefined,
230 | disabled: undefined,
231 | bpmnJsOptions: bpmnJsOptions,
232 | modelerOptions: modelerOptions,
233 | propertiesPanelOptions: propertiesPanelOptions
234 | }), [bpmnJsOptions, modelerOptions, propertiesPanelOptions]);
235 |
236 | return (
237 |
242 |
243 |
280 | Save
281 | Diagram
282 | < /button>
283 |
284 | < BpmnModeler
285 | xml = { xml }
286 | onEvent = { onEvent }
287 | xmlTabOptions = { xmlTabOptions }
288 | modelerTabOptions = { modelerTabOptions }
289 | />
290 | < /div>
291 | )
292 | ;
293 | }
294 |
295 | export default App;
296 |
297 | const BPMN = /* .... */;
298 | ```
299 |
300 | ## Usage with DMN
301 |
302 | Usage with DMN is essentially the same. You just have to use the `` component instead. The API is very
303 | consistent between the two components.
304 |
305 | ## More examples
306 |
307 | You can find more examples in our examples
308 | repository [camunda-web-modeler-examples](https://github.com/FlowSquad/camunda-web-modeler-examples).
309 |
310 | # Usage without npm
311 |
312 | You can also use the modeler without npm. However, you should note that this support is only experimental and not
313 | thoroughly tested.
314 |
315 | **index.html**
316 |
317 | ```html
318 |
319 |
320 |
321 |
322 | Modeler Test
323 |
324 |
325 |
326 |
327 |
328 |
329 |
330 |
331 |
332 |
333 |
334 |
335 |
336 |
337 |
338 |
339 |
340 |
341 |
342 |
343 |
344 |
345 |
346 |
347 |
348 |
350 |
351 |
371 |
372 |
373 |
374 | ```
375 |
376 | **index.js**
377 |
378 | ```javascript
379 | const XML = /* ... */;
380 |
381 | initialize = () => {
382 | var xml = XML;
383 | const domContainer = document.querySelector('#root');
384 |
385 | var render = () => {
386 | ReactDOM.render(React.createElement(MiragonModeler.BpmnModeler, { // or MiragonModeler.DmnModeler
387 | modelerTabOptions: {
388 | propertiesPanelOptions: {
389 | hidden: false
390 | }
391 | },
392 | xml: xml,
393 | onEvent: (event) => {
394 | if (event.source === "modeler" && event.event === "content.saved" && event.data.reason === "view.changed") {
395 | console.log("XML has been saved", xml);
396 | xml = event.data.xml;
397 | render();
398 | }
399 | }
400 | }), domContainer);
401 | };
402 | render();
403 | };
404 |
405 |
406 | ```
407 |
408 | # Issues and Questions
409 |
410 | If you experience any bugs or have questions concerning the usage or further development plans, don't hesitate to create
411 | a new issue. However, **please make sure to include all relevant logs, screenshots, and code examples**. Thanks!
412 |
413 | # API Reference
414 |
415 | For the API reference, start with the type definitions in these files and work your way through:
416 |
417 | - [BpmnModeler.tsx](./src/BpmnModeler.tsx)
418 | - [DmnModeler.tsx](./src/DmnModeler.tsx)
419 |
420 | You can also use the documentation that you can find here:
421 |
422 | - [BPMNModeler](https://unpkg.com/@miragon/camunda-web-modeler@latest/dist/docs/modules/bpmnmodeler.html)
423 | - [DMNModeler](https://unpkg.com/@miragon/camunda-web-modeler@latest/dist/docs/modules/dmnmodeler.html)
424 |
425 | ## Engage with the Miragon team
426 |
427 | If you have any questions or need support, feel free to reach out to us via
428 | email ([info@miragon.io](mailto:info@miragon.io)).
429 | We are here to help you, especially if you are considering introducing camunda-web-modeler in your organization.
430 |
431 | For inquiries and professional support, please contact us at: [info@miragon.io](mailto:info@miragon.io)
432 |
433 | # License
434 |
435 | ```
436 | /**
437 | * Copyright 2021 FlowSquad GmbH
438 | *
439 | * Licensed under the Apache License, Version 2.0 (the "License");
440 | * you may not use this file except in compliance with the License.
441 | * You may obtain a copy of the License at
442 | *
443 | * http://www.apache.org/licenses/LICENSE-2.0
444 | *
445 | * Unless required by applicable law or agreed to in writing, software
446 | * distributed under the License is distributed on an "AS IS" BASIS,
447 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
448 | * See the License for the specific language governing permissions and
449 | * limitations under the License.
450 | */
451 | ```
452 |
453 | For the full license text, see the LICENSE file above.
454 | Remember that the bpmn.io license still applies, i.e. you must not remove the icon displayed in the bottom-right corner.
455 |
--------------------------------------------------------------------------------
/eslint.config.mjs:
--------------------------------------------------------------------------------
1 | import eslint from "@eslint/js";
2 | import tseslint from "typescript-eslint";
3 | import stylistic from "@stylistic/eslint-plugin";
4 | import eslintConfigPrettier from "eslint-config-prettier";
5 | import eslintPluginImport from "eslint-plugin-import";
6 | import eslintPluginReact from "eslint-plugin-react";
7 | import eslintPluginReactHooks from "eslint-plugin-react-hooks";
8 | import eslintPluginFlowtype from "eslint-plugin-flowtype";
9 |
10 | import { fixupPluginRules } from "@eslint/compat";
11 |
12 | // const __filename = fileURLToPath(import.meta.url);
13 | // const __dirname = path.dirname(__filename);
14 | //
15 | // const compat = new FlatCompat({
16 | // baseDirectory: __dirname,
17 | // });
18 |
19 | export default tseslint.config(
20 | eslint.configs.recommended,
21 | ...tseslint.configs.recommendedTypeChecked,
22 | ...tseslint.configs.stylisticTypeChecked,
23 | eslintConfigPrettier,
24 | {
25 | ignores: [
26 | "dist",
27 | "**/*.d.ts",
28 | "eslint.config.mjs",
29 | "rollup.config.mjs",
30 | "index.{js,ts}",
31 | ],
32 | },
33 | {
34 | languageOptions: {
35 | parser: tseslint.parser,
36 | parserOptions: {
37 | project: true,
38 | tsconfigRootDir: import.meta.dirname,
39 | ecmaFeatures: {
40 | jsx: true,
41 | },
42 | },
43 | },
44 | settings: {
45 | react: {
46 | version: "17.0.0",
47 | },
48 | flowtype: {
49 | onlyFilesWithFlowAnnotation: true,
50 | },
51 | },
52 | plugins: {
53 | "@typescript-eslint": tseslint.plugin,
54 | import: eslintPluginImport,
55 | react: eslintPluginReact,
56 | "react-hooks": fixupPluginRules(eslintPluginReactHooks),
57 | "@stylistic": stylistic,
58 | flowtype: eslintPluginFlowtype,
59 | },
60 | rules: {
61 | ...eslintPluginReactHooks.configs.recommended.rules,
62 | "@typescript-eslint/comma-dangle": "off",
63 | "@typescript-eslint/no-unused-expressions": [
64 | "error",
65 | {
66 | allowShortCircuit: true,
67 | allowTernary: true,
68 | },
69 | ],
70 | "@typescript-eslint/no-shadow": [
71 | "error",
72 | {
73 | ignoreFunctionTypeParameterNameValueShadow: true,
74 | },
75 | ],
76 | "@typescript-eslint/object-curly-spacing": "off",
77 | // We need to use any for interfaces towards bpmn-js way too often for this rule to be effective
78 | "@typescript-eslint/no-explicit-any": "off",
79 | "@typescript-eslint/no-unsafe-assignment": "off",
80 | "@typescript-eslint/no-unsafe-member-access": "off",
81 | "@typescript-eslint/no-unsafe-call": "off",
82 | "@typescript-eslint/no-unsafe-return": "off",
83 | "@typescript-eslint/no-unsafe-argument": "off",
84 | "@stylistic/lines-between-class-members": [
85 | "error",
86 | "always",
87 | {
88 | exceptAfterSingleLine: true,
89 | },
90 | ],
91 | "@stylistic/arrow-parens": ["error", "as-needed"],
92 | "react/jsx-indent": [
93 | "error",
94 | 4,
95 | {
96 | indentLogicalExpressions: true,
97 | checkAttributes: true,
98 | },
99 | ],
100 | "react/jsx-indent-props": ["error", 4],
101 | "react/jsx-closing-bracket-location": [
102 | "error",
103 | {
104 | nonEmpty: "tag-aligned",
105 | selfClosing: "tag-aligned",
106 | },
107 | ],
108 | // Does not work with TS and arrow functions
109 | // https://github.com/yannickcr/eslint-plugin-react/issues/2353
110 | "react/prop-types": "off",
111 | // Is this the way to go? I think both ways are acceptable, but should not be enforced...
112 | "react/destructuring-assignment": "off",
113 | // We don't use default props
114 | "react/require-default-props": "off",
115 | "react/jsx-props-no-spreading": "off",
116 | "flowtype/define-flow-type": "off",
117 | "flowtype/use-flow-type": "off",
118 | // Deprecated rule
119 | "object-shorthand": ["error", "consistent-as-needed"],
120 | "no-param-reassign": [
121 | "error",
122 | {
123 | props: false,
124 | },
125 | ],
126 | radix: ["error", "as-needed"],
127 | "object-curly-newline": [
128 | "error",
129 | {
130 | ImportDeclaration: {
131 | multiline: true,
132 | },
133 | },
134 | ],
135 | "no-restricted-syntax": [
136 | "error",
137 | "ForInStatement",
138 | "LabeledStatement",
139 | "WithStatement",
140 | ],
141 | "no-spaced-func": "off",
142 | "import/extensions": [
143 | "error",
144 | "ignorePackages",
145 | {
146 | "": "never",
147 | js: "never",
148 | jsx: "never",
149 | ts: "never",
150 | tsx: "never",
151 | },
152 | ],
153 | "import/no-amd": "off",
154 | },
155 | },
156 | );
157 |
--------------------------------------------------------------------------------
/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./dist";
2 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@miragon/camunda-web-modeler",
3 | "version": "0.1.1",
4 | "description": "A browser-native BPMN and DMN modeler based on bpmn.io.",
5 | "author": "Miragon GmbH ",
6 | "homepage": "https://github.com/Miragon/camunda-web-modeler",
7 | "repository": {
8 | "type": "git",
9 | "url": "git+https://github.com/Miragon/camunda-web-modeler.git"
10 | },
11 | "scripts": {
12 | "clean": "rimraf dist index.js index.d.ts",
13 | "docs": "typedoc src --exclude src/index.ts --exclude src/types --exclude src/components --exclude src/bpmnio --disableSources --out dist/docs",
14 | "watch": "tsc -w",
15 | "build": "tsc && tsc -p tsconfig.index.json && yarn docs && yarn bundle && copyfiles -u 1 \"src/types/*.d.ts\" dist",
16 | "bundle": "rollup -c",
17 | "lint": "eslint -c eslint.config.mjs ."
18 | },
19 | "publishConfig": {
20 | "registry": "https://registry.npmjs.org"
21 | },
22 | "keywords": [
23 | "miragon",
24 | "modeler",
25 | "camunda",
26 | "bpmnio",
27 | "bpmn",
28 | "dmn"
29 | ],
30 | "license": "Apache-2.0",
31 | "files": [
32 | "dist/",
33 | "index.d.ts",
34 | "index.js"
35 | ],
36 | "main": "index.js",
37 | "directories": {
38 | "lib": "dist/"
39 | },
40 | "browserslist": {
41 | "production": [
42 | ">0.2%",
43 | "not dead",
44 | "not op_mini all"
45 | ],
46 | "development": [
47 | "last 1 chrome version",
48 | "last 1 firefox version",
49 | "last 1 safari version"
50 | ]
51 | },
52 | "dependencies": {
53 | "@bpmn-io/element-template-chooser": "^1.0.0",
54 | "@bpmn-io/properties-panel": "^3.24.1",
55 | "@emotion/react": "^11.14.0",
56 | "@monaco-editor/react": "^4.6.0",
57 | "@seznam/compose-react-refs": "^1.0.6",
58 | "@typescript-eslint/eslint-plugin": "^8.24.0",
59 | "bpmn-js": "^18.2.0",
60 | "bpmn-js-element-templates": "^2.5.2",
61 | "bpmn-js-properties-panel": "^5.31.1",
62 | "camunda-bpmn-moddle": "^7.0.1",
63 | "camunda-dmn-moddle": "^1.3.0",
64 | "deepmerge": "^4.3.1",
65 | "diagram-js-origin": "^1.4.0",
66 | "dmn-js": "^17.1.0",
67 | "dmn-js-properties-panel": "^3.7.0",
68 | "monaco-editor": "^0.52.2",
69 | "react-resizable-panels": "^2.1.7",
70 | "tss-react": "^4.9.15"
71 | },
72 | "devDependencies": {
73 | "@babel/eslint-parser": "^7.26.8",
74 | "@eslint/compat": "^1.2.6",
75 | "@eslint/eslintrc": "^3.2.0",
76 | "@eslint/js": "^9.20.0",
77 | "@rollup/plugin-commonjs": "^28.0.2",
78 | "@rollup/plugin-json": "^6.1.0",
79 | "@rollup/plugin-node-resolve": "^16.0.0",
80 | "@rollup/plugin-replace": "^6.0.2",
81 | "@rollup/plugin-terser": "^0.4.4",
82 | "@stylistic/eslint-plugin": "^3.1.0",
83 | "@types/react": "^18.3.18",
84 | "copyfiles": "^2.4.1",
85 | "eslint": "^9.20.0",
86 | "eslint-config-airbnb-typescript": "^18.0.0",
87 | "eslint-config-prettier": "^10.0.1",
88 | "eslint-config-react-app": "^7.0.1",
89 | "eslint-plugin-flowtype": "^8.0.3",
90 | "eslint-plugin-import": "^2.31.0",
91 | "eslint-plugin-jsx-a11y": "^6.10.2",
92 | "eslint-plugin-react": "^7.37.4",
93 | "eslint-plugin-react-app": "^6.2.2",
94 | "eslint-plugin-react-hooks": "^5.1.0",
95 | "globals": "^15.14.0",
96 | "prettier": "^3.5.0",
97 | "rimraf": "^6.0.1",
98 | "rollup": "^4.34.6",
99 | "rollup-plugin-css-only": "^4.5.2",
100 | "rollup-plugin-typescript2": "^0.36.0",
101 | "tslib": "^2.8.1",
102 | "typedoc": "^0.27.7",
103 | "typescript": "^5.7.3",
104 | "typescript-eslint": "^8.24.0"
105 | },
106 | "peerDependencies": {
107 | "react": "^17.0.0 || ^18.0.0"
108 | },
109 | "resolutions": {
110 | "min-dash": "4.1.1"
111 | },
112 | "packageManager": "yarn@4.6.0"
113 | }
114 |
--------------------------------------------------------------------------------
/rollup.config.mjs:
--------------------------------------------------------------------------------
1 | import terser from "@rollup/plugin-terser";
2 | import json from "@rollup/plugin-json";
3 | import commonjs from "@rollup/plugin-commonjs";
4 | import replace from '@rollup/plugin-replace';
5 | import {nodeResolve} from "@rollup/plugin-node-resolve";
6 | import typescript from 'rollup-plugin-typescript2';
7 | import css from "rollup-plugin-css-only";
8 | import deepmerge from "deepmerge";
9 | /* import fs from "fs";
10 | import path from "path"; */
11 |
12 | import pkg from "./package.json" with { type: "json" };
13 |
14 | const defaultConfig = {
15 | input: "src/index.ts",
16 | output: {
17 | format: "umd",
18 | name: "MiragonModeler",
19 | globals: {
20 | react: "React",
21 | "monaco-editor": "monaco",
22 | "bpmn-js/lib/Modeler": "BpmnJS",
23 | "dmn-js/lib/Modeler": "DmnJS"
24 | }
25 | },
26 | external: [
27 | ...Object.keys(pkg.peerDependencies || {}),
28 | "monaco-editor",
29 | // "react",
30 | "bpmn-js/lib/Modeler",
31 | "dmn-js/lib/Modeler"
32 | ],
33 | plugins: [
34 | replace({
35 | values: {
36 | 'process.env.NODE_ENV': JSON.stringify('production')
37 | },
38 | preventAssignment: true
39 | }),
40 | nodeResolve({
41 | mainFields: [
42 | 'browser',
43 | 'module',
44 | 'main'
45 | ]
46 | }),
47 | commonjs({
48 | exclude: ["src/**"],
49 | include: ["node_modules/**"]
50 | }),
51 | json(),
52 | typescript(),
53 | css({output: "bundle.css"})
54 | ]
55 | };
56 |
57 | export default [
58 | /* deepmerge(defaultConfig, {
59 | output: {
60 | file: "dist/bundle.js"
61 | }
62 | }), */
63 | deepmerge(defaultConfig, {
64 | output: {
65 | file: "dist/bundle.min.js",
66 | sourcemap: true
67 | },
68 | plugins: [
69 | terser(),
70 | /* { // Use this to write a graph.json to analyze the bundle
71 | // Remember to uncomment the imports, too
72 | buildEnd() {
73 | const deps = [];
74 | for (const id of this.getModuleIds()) {
75 | const m = this.getModuleInfo(id);
76 | if (m != null && !m.isExternal) {
77 | for (const target of m.importedIds) {
78 | deps.push({source: m.id, target})
79 | }
80 | }
81 | }
82 |
83 | fs.writeFileSync(
84 | path.join(__dirname, 'graph.json'),
85 | JSON.stringify(deps, null, 2));
86 | },
87 | } */
88 | ]
89 | })
90 | ];
91 |
--------------------------------------------------------------------------------
/src/BpmnModeler.tsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
2 | import { tss } from "tss-react";
3 | import * as monaco from "monaco-editor";
4 |
5 | import CustomBpmnJsModeler from "./bpmnio/bpmn/CustomBpmnJsModeler";
6 | import SvgIcon from "./components/SvgIcon";
7 | import ToggleGroup from "./components/ToggleGroup";
8 | import BpmnEditor, {
9 | BpmnModelerOptions,
10 | BpmnPropertiesPanelOptions,
11 | } from "./editor/BpmnEditor";
12 | import XmlEditor, { MonacoOptions } from "./editor/XmlEditor";
13 | import { Event } from "./events";
14 | import {
15 | ContentSavedReason,
16 | createContentSavedEvent,
17 | } from "./events/modeler/ContentSavedEvent";
18 |
19 | const useStyles = tss.create(() => ({
20 | root: {
21 | height: "100%",
22 | overflow: "hidden",
23 | },
24 | modeToggle: {
25 | position: "absolute",
26 | left: "97px",
27 | bottom: "32px",
28 | backgroundColor: "rgba(255, 255, 255, 0.87)",
29 | },
30 | icon: {
31 | marginTop: "4px",
32 | },
33 | }));
34 |
35 | export interface ModelerTabOptions {
36 | /**
37 | * This option disables the modeler tab.
38 | */
39 | disabled?: boolean;
40 |
41 | /**
42 | * The options passed to the bpmn-js modeler.
43 | *
44 | * CAUTION: When this option object is changed, the old editor instance will be destroyed
45 | * and a new one will be created without automatic saving!
46 | */
47 | bpmnJsOptions?: any;
48 |
49 | /**
50 | * The options to control the appearance of the properties panel.
51 | */
52 | propertiesPanelOptions?: BpmnPropertiesPanelOptions;
53 |
54 | /**
55 | * The options to control the appearance of the modeler.
56 | */
57 | modelerOptions?: BpmnModelerOptions;
58 |
59 | /**
60 | * The class name to apply to the modeler tab root element.
61 | */
62 | className?: string;
63 | }
64 |
65 | export interface XmlTabOptions {
66 | /**
67 | * This option disables the XML tab.
68 | */
69 | disabled?: boolean;
70 |
71 | /**
72 | * The options to pass to the monaco editor.
73 | */
74 | monacoOptions?: MonacoOptions;
75 |
76 | /**
77 | * The class name applied to the host of the modeler.
78 | */
79 | className?: string;
80 | }
81 |
82 | export interface BpmnModelerProps {
83 | /**
84 | * The class name applied to the root element.
85 | */
86 | className?: string;
87 |
88 | /**
89 | * The XML to display in the editor.
90 | */
91 | xml: string;
92 |
93 | /**
94 | * Called whenever an event occurs.
95 | */
96 | onEvent: (event: Event) => void;
97 |
98 | /**
99 | * Options to customize the modeler tab.
100 | */
101 | modelerTabOptions?: ModelerTabOptions;
102 |
103 | /**
104 | * Options to customize the XML tab.
105 | */
106 | xmlTabOptions?: XmlTabOptions;
107 | }
108 |
109 | declare type BpmnViewMode = "bpmn" | "xml";
110 |
111 | const BpmnModeler: React.FC = props => {
112 | const { classes, cx } = useStyles();
113 |
114 | const { onEvent, xml, modelerTabOptions, xmlTabOptions, className } = props;
115 |
116 | const monacoRef = useRef(null);
117 | const modelerRef = useRef();
118 |
119 | const [mode, setMode] = useState("bpmn");
120 |
121 | useEffect(() => {
122 | if (modelerTabOptions?.disabled && !xmlTabOptions?.disabled) {
123 | setMode("xml");
124 | }
125 | }, [modelerTabOptions, xmlTabOptions]);
126 |
127 | const modelerOptions: BpmnModelerOptions = useMemo(() => {
128 | if (!modelerTabOptions?.modelerOptions) {
129 | return {
130 | refs: [modelerRef],
131 | };
132 | }
133 |
134 | return {
135 | ...modelerTabOptions.modelerOptions,
136 | refs: [...(modelerTabOptions.modelerOptions.refs ?? []), modelerRef],
137 | };
138 | }, [modelerTabOptions]);
139 |
140 | const monacoOptions: MonacoOptions = useMemo(() => {
141 | if (!xmlTabOptions?.monacoOptions) {
142 | return {
143 | refs: [monacoRef],
144 | };
145 | }
146 |
147 | return {
148 | ...xmlTabOptions.monacoOptions,
149 | refs: [...(xmlTabOptions.monacoOptions.refs ?? []), monacoRef],
150 | };
151 | }, [xmlTabOptions]);
152 |
153 | const saveFile = useCallback(
154 | async (source: BpmnViewMode, reason: ContentSavedReason) => {
155 | switch (source) {
156 | case "bpmn": {
157 | if (modelerRef.current) {
158 | const saved = await modelerRef.current?.save();
159 | onEvent(createContentSavedEvent(saved.xml, saved.svg, reason));
160 | }
161 | break;
162 | }
163 | case "xml": {
164 | if (monacoRef.current) {
165 | //const saved = await monacoRef.current?.editor?.getValue() || "";
166 | const saved = monacoRef.current?.getValue() || "";
167 | onEvent(createContentSavedEvent(saved, undefined, reason));
168 | }
169 | break;
170 | }
171 | }
172 | },
173 | [onEvent],
174 | );
175 |
176 | const changeMode = useCallback(
177 | async (value: string) => {
178 | const bpmnViewMode = value as BpmnViewMode;
179 | if (bpmnViewMode !== null && bpmnViewMode !== mode) {
180 | await saveFile(mode, "view.changed");
181 | setMode(bpmnViewMode);
182 | }
183 | },
184 | [saveFile, mode],
185 | );
186 |
187 | const onXmlChanged = useCallback(
188 | (value: string) => {
189 | onEvent(createContentSavedEvent(value, undefined, "xml.changed"));
190 | },
191 | [onEvent],
192 | );
193 |
194 | if (!xml) {
195 | return null;
196 | }
197 |
198 | return (
199 |
200 | {!modelerTabOptions?.disabled && (
201 |
210 | )}
211 |
212 | {!xmlTabOptions?.disabled && (
213 |
219 | )}
220 |
221 | {!xmlTabOptions?.disabled && !modelerTabOptions?.disabled && (
222 |
234 | ),
235 | },
236 | {
237 | id: "xml",
238 | node: (
239 |
244 | ),
245 | },
246 | ]}
247 | onChange={changeMode}
248 | active={mode}
249 | />
250 | )}
251 |
252 | );
253 | };
254 |
255 | export default BpmnModeler;
256 |
--------------------------------------------------------------------------------
/src/DmnModeler.tsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
2 | import { tss } from "tss-react";
3 | import * as monaco from "monaco-editor";
4 |
5 | import CustomDmnJsModeler, {
6 | DmnView,
7 | ViewsChangedEvent,
8 | } from "./bpmnio/dmn/CustomDmnJsModeler";
9 | import SvgIcon from "./components/SvgIcon";
10 | import ToggleGroup from "./components/ToggleGroup";
11 | import DmnEditor, {
12 | DmnModelerOptions,
13 | DmnPropertiesPanelOptions,
14 | } from "./editor/DmnEditor";
15 | import XmlEditor, { MonacoOptions } from "./editor/XmlEditor";
16 | import { Event, isBpmnIoEvent } from "./events";
17 | import {
18 | ContentSavedReason,
19 | createContentSavedEvent,
20 | } from "./events/modeler/ContentSavedEvent";
21 |
22 | export interface ModelerTabOptions {
23 | /**
24 | * This option disables the modeler tab.
25 | */
26 | disabled?: boolean;
27 |
28 | /**
29 | * The options passed to the bpmn-js modeler.
30 | *
31 | * CAUTION: When this option object is changed, the old editor instance will be destroyed
32 | * and a new one will be created without automatic saving!
33 | */
34 | dmnJsOptions?: any;
35 |
36 | /**
37 | * The options to control the appearance of the properties panel.
38 | */
39 | propertiesPanelOptions?: DmnPropertiesPanelOptions;
40 |
41 | /**
42 | * The options to control the appearance of the modeler.
43 | */
44 | modelerOptions?: DmnModelerOptions;
45 |
46 | /**
47 | * The class name to apply to the modeler tab root element.
48 | */
49 | className?: string;
50 | }
51 |
52 | export interface XmlTabOptions {
53 | /**
54 | * This option disables the XML tab.
55 | */
56 | disabled?: boolean;
57 |
58 | /**
59 | * The options to pass to the monaco editor.
60 | */
61 | monacoOptions?: MonacoOptions;
62 |
63 | /**
64 | * The class name applied to the host of the modeler.
65 | */
66 | className?: string;
67 | }
68 |
69 | export interface DmnModelerProps {
70 | /**
71 | * The class name applied to the root element.
72 | */
73 | className?: string;
74 |
75 | /**
76 | * The xml to display in the editor.
77 | */
78 | xml: string;
79 |
80 | /**
81 | * Called whenever an event occurs.
82 | */
83 | onEvent: (event: Event) => void;
84 |
85 | /**
86 | * Options to customize the modeler tab.
87 | */
88 | modelerTabOptions?: ModelerTabOptions;
89 |
90 | /**
91 | * Options to customize the XML tab.
92 | */
93 | xmlTabOptions?: XmlTabOptions;
94 | }
95 |
96 | const useStyles = tss.create(() => ({
97 | root: {
98 | height: "100%",
99 | overflow: "hidden",
100 | },
101 | modeToggle: {
102 | position: "absolute",
103 | left: "32px",
104 | bottom: "32px",
105 | backgroundColor: "rgba(255, 255, 255, 0.87)",
106 | },
107 | buttonTitle: {
108 | marginLeft: "0.5rem",
109 | textTransform: "none",
110 | maxWidth: "8rem",
111 | textOverflow: "ellipsis",
112 | overflow: "hidden",
113 | whiteSpace: "nowrap",
114 | },
115 | icon: {
116 | marginTop: "4px",
117 | },
118 | }));
119 |
120 | const DmnModeler: React.FC = props => {
121 | const { classes, cx } = useStyles();
122 |
123 | const { onEvent, className, xmlTabOptions, modelerTabOptions, xml } = props;
124 |
125 | const monacoRef = useRef(null);
126 | const modelerRef = useRef();
127 |
128 | const [views, setViews] = useState([]);
129 | const [activeView, setActiveView] = useState(undefined);
130 |
131 | useEffect(() => {
132 | if (modelerTabOptions?.disabled && !xmlTabOptions?.disabled) {
133 | setActiveView("xml");
134 | }
135 | }, [modelerTabOptions, xmlTabOptions]);
136 |
137 | const saveFile = useCallback(
138 | async (source: string | undefined, reason: ContentSavedReason) => {
139 | switch (source) {
140 | case "xml": {
141 | if (monacoRef.current) {
142 | const saved = monacoRef.current?.getValue() || "";
143 | onEvent(createContentSavedEvent(saved, undefined, reason));
144 | }
145 | break;
146 | }
147 | case undefined: {
148 | break;
149 | }
150 | default: {
151 | if (modelerRef.current) {
152 | const saved = await modelerRef.current?.save({ format: true });
153 | onEvent(createContentSavedEvent(saved.xml, undefined, reason));
154 | }
155 | break;
156 | }
157 | }
158 | },
159 | [onEvent],
160 | );
161 |
162 | const changeMode = useCallback(
163 | async (viewId: string | undefined) => {
164 | if (viewId && activeView !== viewId) {
165 | // View has been changed from or to XML, save it so the user can reimport it
166 | if (viewId === "xml" || activeView === "xml") {
167 | await saveFile(activeView, "view.changed");
168 | }
169 |
170 | setActiveView(viewId);
171 |
172 | // View is a dmn-js view, open it
173 | if (viewId !== "xml") {
174 | const view = modelerRef.current
175 | ?.getViews()
176 | .find(v => v.id === viewId);
177 | await (view && modelerRef.current?.open(view));
178 | }
179 | }
180 | },
181 | [activeView, saveFile],
182 | );
183 |
184 | const localOnEvent = useCallback(
185 | (event: Event) => {
186 | if (isBpmnIoEvent(event) && event.event === "views.changed" && event.data) {
187 | const data = event.data as ViewsChangedEvent;
188 | setViews(data.views);
189 |
190 | if (!data.activeView) {
191 | const initialView = data.views[0];
192 | setActiveView(initialView.id);
193 | modelerRef.current
194 | ?.open(data.views[0])
195 | .then(result => {
196 | if (result.warnings.length > 0) {
197 | console.log(
198 | "Opened initial view with warnings",
199 | result.warnings,
200 | );
201 | }
202 | })
203 | .catch((e: any) => {
204 | if (e.warnings) {
205 | console.log(
206 | "Failed to open initial view with warnings",
207 | e.warnings,
208 | e.error,
209 | );
210 | }
211 | });
212 | } else {
213 | setActiveView(data.activeView.id);
214 | }
215 | }
216 | onEvent(event);
217 | },
218 | [onEvent],
219 | );
220 |
221 | const onXmlChanged = useCallback(
222 | (value: string) => {
223 | onEvent(createContentSavedEvent(value, undefined, "xml.changed"));
224 | },
225 | [onEvent],
226 | );
227 |
228 | const modelerOptions: DmnModelerOptions = useMemo(() => {
229 | if (!modelerTabOptions?.modelerOptions) {
230 | return {
231 | refs: [modelerRef],
232 | };
233 | }
234 |
235 | return {
236 | ...modelerTabOptions.modelerOptions,
237 | refs: [...(modelerTabOptions.modelerOptions.refs ?? []), modelerRef],
238 | };
239 | }, [modelerTabOptions]);
240 |
241 | const monacoOptions: MonacoOptions = useMemo(() => {
242 | if (!xmlTabOptions?.monacoOptions) {
243 | return {
244 | refs: [monacoRef],
245 | };
246 | }
247 |
248 | return {
249 | ...xmlTabOptions.monacoOptions,
250 | refs: [...(xmlTabOptions.monacoOptions.refs ?? []), monacoRef],
251 | };
252 | }, [xmlTabOptions]);
253 |
254 | if (!xml) {
255 | return null;
256 | }
257 |
258 | return (
259 |
260 | {!modelerTabOptions?.disabled && (
261 |
270 | )}
271 |
272 | {!xmlTabOptions?.disabled && (
273 |
279 | )}
280 |
281 | {!modelerTabOptions?.disabled && (
282 | ({
286 | id: view.id,
287 | node: (
288 | <>
289 |
298 |
299 |
303 | {view.name || "Unnamed"}
304 |
305 | >
306 | ),
307 | })),
308 | {
309 | id: "xml",
310 | node: (
311 |
316 | ),
317 | },
318 | ]}
319 | onChange={changeMode}
320 | active={activeView ?? ""}
321 | />
322 | )}
323 |
324 | );
325 | };
326 |
327 | export default DmnModeler;
328 |
--------------------------------------------------------------------------------
/src/bpmnio/GlobalEventListenerUtil.ts:
--------------------------------------------------------------------------------
1 | export type EventCallback = (event: string, data: any) => void;
2 |
3 | /**
4 | * A module that hooks into the event bus fire method to dispatch all events to the callbacks
5 | * registered via the on() method.
6 | */
7 | class GlobalEventListenerUtil {
8 | private listeners: EventCallback[] = [];
9 |
10 | constructor(eventBus: any) {
11 | const fire = eventBus.fire.bind(eventBus);
12 |
13 | eventBus.fire = (event: string, data: any) => {
14 | this.listeners.forEach(l => l(event, data));
15 | return fire(event, data);
16 | };
17 | }
18 |
19 | /**
20 | * Registers a new callback that will receive all events fired by bpmn.io.
21 | *
22 | * @param callback The callback to register
23 | */
24 | public on = (callback: EventCallback): void => {
25 | if (!this.listeners.includes(callback)) {
26 | this.listeners.push(callback);
27 | }
28 | };
29 |
30 | /**
31 | * Unregisters a previously registered callback.
32 | *
33 | * @param callback The callback to unregister
34 | */
35 | public off = (callback: EventCallback): void => {
36 | this.listeners = this.listeners.filter(l => l !== callback);
37 | };
38 | }
39 |
40 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment
41 | // @ts-ignore
42 | GlobalEventListenerUtil.$inject = ["eventBus"];
43 |
44 | export default GlobalEventListenerUtil;
45 |
--------------------------------------------------------------------------------
/src/bpmnio/bpmn/CustomBpmnJsModeler.ts:
--------------------------------------------------------------------------------
1 | import "@bpmn-io/properties-panel/dist/assets/properties-panel.css";
2 | import "bpmn-js-element-templates/dist/assets/element-templates.css";
3 | import "@bpmn-io/element-template-chooser/dist/element-template-chooser.css";
4 | import "bpmn-js/dist/assets/bpmn-font/css/bpmn-embedded.css";
5 | import "bpmn-js/dist/assets/diagram-js.css";
6 | import camundaModdleDescriptor from "camunda-bpmn-moddle/resources/camunda.json";
7 | import {
8 | BpmnPropertiesPanelModule,
9 | BpmnPropertiesProviderModule,
10 | } from "bpmn-js-properties-panel";
11 | import { ElementTemplatesPropertiesProviderModule } from "bpmn-js-element-templates";
12 | import ElementTemplateChooserModule from "@bpmn-io/element-template-chooser";
13 | import Modeler from "bpmn-js/lib/Modeler";
14 | import deepmerge from "deepmerge";
15 | import GlobalEventListenerUtil, { EventCallback } from "../GlobalEventListenerUtil";
16 |
17 | export interface CustomBpmnJsModelerOptions {
18 | /**
19 | * The ID of the div to use as host for the properties panel. The div must be present inside
20 | * the page HTML. If missing or undefined is passed, no properties panel will be initialized.
21 | */
22 | propertiesPanel?: string;
23 |
24 | /**
25 | * The ID of the div to use as host for the editor itself. The div must be present inside the
26 | * page HTML.
27 | */
28 | container: string;
29 |
30 | /**
31 | * The options passed to bpmn-js. Will be merged with the options defined by this library,
32 | * with the latter taking precedence in case of conflict.
33 | * CAUTION: If you pass invalid properties, the modeler can break!
34 | */
35 | bpmnJsOptions?: any;
36 | }
37 |
38 | interface Injector {
39 | /**
40 | * Returns a named component.
41 | *
42 | * @param name The name
43 | * @param strict If an error should be thrown if the component does not exist. If false, null
44 | * will be returned.
45 | */
46 | get: (name: string, strict?: boolean) => any;
47 | }
48 |
49 | class CustomBpmnJsModeler extends Modeler {
50 | /**
51 | * Creates a new instance of the bpmn-js modeler.
52 | *
53 | * @param options The options to include
54 | */
55 | constructor(options: CustomBpmnJsModelerOptions) {
56 | const mergedOptions = deepmerge.all(
57 | [
58 | // The options passed by the user
59 | options.bpmnJsOptions || {},
60 |
61 | // The library's default options
62 | {
63 | container: options.container,
64 | additionalModules: [
65 | {
66 | __init__: ["globalEventListenerUtil"],
67 | globalEventListenerUtil: ["type", GlobalEventListenerUtil],
68 | },
69 | ],
70 | moddleExtensions: {
71 | camunda: camundaModdleDescriptor,
72 | },
73 | },
74 |
75 | // The options required to display the properties panel (if desired)
76 | // prettier-ignore
77 | options.propertiesPanel
78 | ? {
79 | propertiesPanel: {
80 | parent: options.propertiesPanel,
81 | },
82 | additionalModules: [
83 | BpmnPropertiesPanelModule,
84 | BpmnPropertiesProviderModule,
85 | ElementTemplatesPropertiesProviderModule,
86 | ElementTemplateChooserModule,
87 | ],
88 | }
89 | : {},
90 | ],
91 | {
92 | // Deprecated, but @dominikhorn93 said it's okay because it's gonna stay that way for
93 | // at least 5 years (or forever)
94 | clone: false,
95 | },
96 | );
97 | super(mergedOptions);
98 | }
99 |
100 | /**
101 | * Returns the injector.
102 | */
103 | getInjector(): Injector {
104 | return this.get("injector");
105 | }
106 |
107 | /**
108 | * Saves the editor content as SVG and XML simultaneously.
109 | */
110 | async save(): Promise<{ xml: string; svg: string }> {
111 | const [{ xml }, { svg }] = await Promise.all([
112 | this.saveXML({
113 | format: true,
114 | preamble: false,
115 | }),
116 | this.saveSVG(),
117 | ]);
118 | return { xml, svg };
119 | }
120 |
121 | /**
122 | * Registers a global event listener that will receive all bpmn-js events.
123 | *
124 | * @param listener The listener to register
125 | */
126 | public registerGlobalEventListener(listener: EventCallback): void {
127 | this.get("globalEventListenerUtil").on(listener);
128 | }
129 |
130 | /**
131 | * Unregisters a previously registered global event listener.
132 | *
133 | * @param listener The listener to unregister
134 | */
135 | public unregisterGlobalEventListener(listener: EventCallback): void {
136 | this.get("globalEventListenerUtil").off(listener);
137 | }
138 |
139 | /**
140 | * Imports element templates into the editor.
141 | *
142 | * @param elementTemplates The element templates to import.
143 | */
144 | public importElementTemplates(elementTemplates: Record[]): void {
145 | this.get("elementTemplatesLoader").setTemplates(elementTemplates);
146 | }
147 |
148 | /**
149 | * Toggles the hand tool.
150 | */
151 | public toggleHandTool(): void {
152 | this.getInjector().get("handTool").toggle();
153 | }
154 |
155 | /**
156 | * Toggles the lasso tool.
157 | */
158 | public toggleLassoTool(): void {
159 | this.getInjector().get("lassoTool").toggle();
160 | }
161 |
162 | /**
163 | * Toggles the space tool.
164 | */
165 | public toggleSpaceTool(): void {
166 | this.getInjector().get("spaceTool").toggle();
167 | }
168 |
169 | /**
170 | * Toggles the global connect tool.
171 | */
172 | public toggleGlobalConnectTool(): void {
173 | this.getInjector().get("globalConnect").toggle();
174 | }
175 |
176 | /**
177 | * Toggles the search box.
178 | */
179 | public toggleFind(): void {
180 | this.getInjector().get("searchPad").toggle();
181 | }
182 |
183 | /**
184 | * Activates the edit label function.
185 | */
186 | public toggleEditLabel(): void {
187 | const selection = this.get("selection").get();
188 | if (selection.length > 0) {
189 | this.getInjector().get("directEditing").activate(selection[0]);
190 | }
191 | }
192 |
193 | /**
194 | * Expands the selection to all available elements.
195 | */
196 | public selectAll(): void {
197 | const canvas = this.getInjector().get("canvas");
198 | const elementRegistry = this.getInjector().get("elementRegistry");
199 | const selection = this.get("selection");
200 |
201 | // select all elements except for the invisible
202 | // root element
203 | const rootElement = canvas.getRootElement();
204 | const elements = elementRegistry.filter(
205 | (element: any) => element !== rootElement,
206 | );
207 | selection.select(elements);
208 | }
209 |
210 | /**
211 | * Removes the currently selected elements.
212 | */
213 | public removeSelected(): void {
214 | const modeling = this.get("modeling");
215 | const selectedElements = this.getInjector().get("selection").get();
216 |
217 | if (selectedElements.length === 0) {
218 | return;
219 | }
220 |
221 | modeling.removeElements(selectedElements.slice());
222 | }
223 |
224 | /**
225 | * Returns the size of the current selection.
226 | */
227 | public getSelectionSize(): number {
228 | return this.get("selection").get().length;
229 | }
230 |
231 | /**
232 | * Returns whether there is any element selected that can be copied.
233 | * Can be used to determine if the copy button should be enabled or not.
234 | */
235 | public canCopy(): boolean {
236 | return this.get("selection").get().length > 0;
237 | }
238 |
239 | /**
240 | * Returns whether there is any element in the clipboard that can be pasted.
241 | * Can be used to determine if the paste button should be enabled or not.
242 | */
243 | public canPaste(): boolean {
244 | return !this.get("clipboard").isEmpty();
245 | }
246 |
247 | /**
248 | * Returns the current stack index.
249 | */
250 | public getStackIndex(): number {
251 | return this.get("commandStack")._stackIdx;
252 | }
253 |
254 | /**
255 | * Returns whether the command stack contains any actions that can be undone.
256 | * Can be used to determine if the undo button should be enabled or not.
257 | */
258 | public canUndo(): boolean {
259 | return this.get("commandStack").canUndo();
260 | }
261 |
262 | /**
263 | * Returns whether the command stack contains any actions that can be repeated.
264 | * Can be used to determine if the redo button should be enabled or not.
265 | */
266 | public canRedo(): boolean {
267 | return this.get("commandStack").canRedo();
268 | }
269 |
270 | /**
271 | * Instructs the command stack to undo the last action.
272 | */
273 | public undo(): void {
274 | return this.get("commandStack").undo();
275 | }
276 |
277 | /**
278 | * Instructs the command stack to repeat the last undone action.
279 | */
280 | public redo(): void {
281 | return this.get("commandStack").redo();
282 | }
283 |
284 | /**
285 | * Resets the zoom level to its default value.
286 | */
287 | public resetZoom(): void {
288 | this.get("zoomScroll").reset();
289 | }
290 | }
291 |
292 | export default CustomBpmnJsModeler;
293 |
--------------------------------------------------------------------------------
/src/bpmnio/dmn/CustomDmnJsModeler.ts:
--------------------------------------------------------------------------------
1 | import "@bpmn-io/properties-panel/dist/assets/properties-panel.css";
2 | import "dmn-js/dist/assets/diagram-js.css";
3 | import "dmn-js/dist/assets/dmn-font/css/dmn-embedded.css";
4 | import "dmn-js/dist/assets/dmn-js-decision-table-controls.css";
5 | import "dmn-js/dist/assets/dmn-js-decision-table.css";
6 | import "dmn-js/dist/assets/dmn-js-drd.css";
7 | import "dmn-js/dist/assets/dmn-js-literal-expression.css";
8 | import "dmn-js/dist/assets/dmn-js-shared.css";
9 | import camundaModdleDescriptor from "camunda-dmn-moddle/resources/camunda.json";
10 |
11 | import {
12 | DmnPropertiesPanelModule,
13 | DmnPropertiesProviderModule,
14 | } from "dmn-js-properties-panel";
15 | import deepmerge from "deepmerge";
16 | import diagramOriginModule from "diagram-js-origin";
17 | import Modeler, {
18 | ImportXMLResult,
19 | OpenError,
20 | OpenResult,
21 | SaveXMLResult,
22 | } from "dmn-js/lib/Modeler";
23 | import GlobalEventListenerUtil, { EventCallback } from "../GlobalEventListenerUtil";
24 |
25 | export interface ViewsChangedEvent {
26 | activeView: DmnView | undefined;
27 | views: DmnView[];
28 | }
29 |
30 | export interface DmnView {
31 | element: any;
32 | id: string;
33 | name: string;
34 | type: "drd" | "decisionTable" | "literalExpression";
35 | }
36 |
37 | export interface DmnViewer {
38 | /**
39 | * Returns a named component.
40 | *
41 | * @param name The name
42 | * @param strict If an error should be thrown if the component does not exist. If false, null
43 | * will be returned.
44 | */
45 | get: (name: string, strict?: boolean) => any;
46 |
47 | /**
48 | * Registers a new event handler.
49 | *
50 | * @param event The event name
51 | * @param handler The handler
52 | */
53 | on: (event: string, handler: (event: any) => void) => void;
54 |
55 | /**
56 | * Unregisters a previously registered event handler.
57 | *
58 | * @param event The event name
59 | * @param handler The handler
60 | */
61 | off: (event: string, handler: (event: any) => void) => void;
62 | }
63 |
64 | export interface CustomDmnJsModelerOptions {
65 | /**
66 | * The ID of the div to use as host for the properties panel. The div must be present inside
67 | * the page HTML. If missing or undefined is passed, no properties panel will be initialized.
68 | */
69 | propertiesPanel?: string;
70 |
71 | /**
72 | * The ID of the div to use as host for the editor itself. The div must be present inside the
73 | * page HTML.
74 | */
75 | container: string;
76 |
77 | /**
78 | * The options passed to dmn-js. Will be merged with the options defined by this library,
79 | * with the latter taking precedence in case of conflict.
80 | * CAUTION: If you pass invalid properties, the modeler can break!
81 | */
82 | dmnJsOptions?: any;
83 | }
84 |
85 | // eslint-disable-next-line @typescript-eslint/no-unused-vars
86 | interface Injector {
87 | /**
88 | * Returns a named component.
89 | *
90 | * @param name The name
91 | * @param strict If an error should be thrown if the component does not exist. If false, null
92 | * will be returned.
93 | */
94 | get: (name: string, strict?: boolean) => any;
95 | }
96 |
97 | class CustomDmnJsModeler {
98 | private modeler: Modeler;
99 |
100 | /**
101 | * Creates a new instance of the bpmn-js modeler.
102 | *
103 | * @param options The options to include
104 | */
105 | constructor(options: CustomDmnJsModelerOptions) {
106 | const mergedOptions = deepmerge.all([
107 | // The options passed by the user
108 | options.dmnJsOptions || {},
109 |
110 | // The library's default options
111 | {
112 | container: options.container,
113 | drd: {
114 | additionalModules: [
115 | diagramOriginModule,
116 | {
117 | __init__: ["globalEventListenerUtil"],
118 | globalEventListenerUtil: ["type", GlobalEventListenerUtil],
119 | },
120 | ],
121 | },
122 | decisionTable: {
123 | additionalModules: [
124 | {
125 | __init__: ["globalEventListenerUtil"],
126 | globalEventListenerUtil: ["type", GlobalEventListenerUtil],
127 | },
128 | ],
129 | },
130 | literalExpression: {
131 | additionalModules: [
132 | {
133 | __init__: ["globalEventListenerUtil"],
134 | globalEventListenerUtil: ["type", GlobalEventListenerUtil],
135 | },
136 | ],
137 | },
138 | moddleExtensions: {
139 | camunda: camundaModdleDescriptor,
140 | },
141 | },
142 |
143 | // The options required to display the properties panel (if desired)
144 | // prettier-ignore
145 | options.propertiesPanel
146 | ? {
147 | drd: {
148 | propertiesPanel: {
149 | parent: options.propertiesPanel,
150 | },
151 | additionalModules: [
152 | DmnPropertiesPanelModule,
153 | DmnPropertiesProviderModule,
154 | ],
155 | },
156 | }
157 | : {},
158 | ]);
159 |
160 | this.modeler = new Modeler(mergedOptions);
161 | }
162 |
163 | /**
164 | * Saves the editor content as XML.
165 | */
166 | public save(params: { format: boolean }): Promise {
167 | return this.modeler.saveXML(params);
168 | }
169 |
170 | /**
171 | * Imports the specified XML.
172 | *
173 | * @param xml The XML to import
174 | * @param open Whether to open the view after importing
175 | */
176 | public import(xml: string, open = true): Promise {
177 | class ImportXMLError extends Error {
178 | warnings: string[];
179 |
180 | constructor(message: string, warnings: string[]) {
181 | super(message);
182 | this.warnings = warnings;
183 | this.name = "ImportXMLError";
184 | }
185 | }
186 |
187 | try {
188 | return this.modeler.importXML(xml, { open });
189 | } catch (error) {
190 | if (error instanceof ImportXMLError) {
191 | console.error(
192 | "Importing XML failed with warnings",
193 | error.warnings,
194 | error,
195 | );
196 | } else {
197 | console.error("Importing XML failed", error);
198 | }
199 | throw error;
200 | }
201 | }
202 |
203 | public get(param: any) {
204 | return this.modeler.get(param);
205 | }
206 |
207 | /**
208 | * Registers an event listener for bpmn-js.
209 | *
210 | * @param event The name of the event
211 | * @param handler The listener to register
212 | */
213 | public on(event: string, handler: (event: any, data: any) => void) {
214 | return this.modeler.on(event, handler);
215 | }
216 |
217 | /**
218 | * Unregisters a previously registered listener for bpmn-js.
219 | *
220 | * @param event The name of the event
221 | * @param handler The previously registered listener to unregister
222 | */
223 | public off(event: string, handler: (event: any, data: any) => void) {
224 | return this.modeler.off(event, handler);
225 | }
226 |
227 | /**
228 | * Returns the active viewer.
229 | */
230 | public getActiveViewer(): DmnViewer | undefined {
231 | return this.modeler.getActiveViewer();
232 | }
233 |
234 | /**
235 | * Returns all available views.
236 | */
237 | public getViews(): DmnView[] {
238 | return this.modeler.getViews();
239 | }
240 |
241 | /**
242 | * Returns the active view.
243 | */
244 | public getActiveView(): DmnView | undefined {
245 | return this.modeler.getActiveView();
246 | }
247 |
248 | /**
249 | * Opens the specified view.
250 | *
251 | * @param view The view to open
252 | */
253 | public open(view: DmnView): Promise {
254 | try {
255 | return this.modeler.open(view);
256 | } catch (error: any) {
257 | if (error.warnings) {
258 | const e = error as OpenError;
259 | console.error("Opening view failed with warnings", e.warnings, e.error);
260 | } else {
261 | console.error("Opening view failed", error);
262 | }
263 | throw error;
264 | }
265 | }
266 |
267 | /**
268 | * Destroys the modeler instance.
269 | */
270 | public destroy() {
271 | this.modeler.destroy();
272 | }
273 |
274 | /**
275 | * Returns whether the command stack contains any actions that can be undone.
276 | * Can be used to determine if the undo button should be enabled or not.
277 | */
278 | public canUndo(): boolean {
279 | return this.modeler.getActiveViewer()?.get("commandStack").canUndo();
280 | }
281 |
282 | /**
283 | * Returns whether the command stack contains any actions that can be repeated.
284 | * Can be used to determine if the redo button should be enabled or not.
285 | */
286 | public canRedo(): boolean {
287 | return this.modeler.getActiveViewer()?.get("commandStack").canRedo();
288 | }
289 |
290 | /**
291 | * Returns the size of the current selection.
292 | */
293 | public getSelectionSize(): number {
294 | return this.modeler.getActiveViewer()?.get("selection")?.get()?.length || 0;
295 | }
296 |
297 | /**
298 | * Returns the current stack index.
299 | */
300 | public getStackIndex(): number {
301 | return this.modeler.getActiveViewer()?.get("commandStack")._stackIdx;
302 | }
303 |
304 | /**
305 | * Instructs the command stack to undo the last action.
306 | */
307 | public undo(): void {
308 | return this.modeler.getActiveViewer()?.get("commandStack").undo();
309 | }
310 |
311 | /**
312 | * Instructs the command stack to repeat the last undone action.
313 | */
314 | public redo(): void {
315 | return this.modeler.getActiveViewer()?.get("commandStack").redo();
316 | }
317 |
318 | /**
319 | * Registers a global event listener that will receive all bpmn-js events.
320 | *
321 | * @param listener The listener to register
322 | */
323 | public registerGlobalEventListener(listener: EventCallback): void {
324 | this.modeler.getActiveViewer()?.get("globalEventListenerUtil").on(listener);
325 | }
326 |
327 | /**
328 | * Unregisters a previously registered global event listener.
329 | *
330 | * @param listener The listener to unregister
331 | */
332 | public unregisterGlobalEventListener(listener: EventCallback): void {
333 | this.modeler.getActiveViewer()?.get("globalEventListenerUtil").off(listener);
334 | }
335 | }
336 |
337 | export default CustomDmnJsModeler;
338 |
--------------------------------------------------------------------------------
/src/bpmnio/index.ts:
--------------------------------------------------------------------------------
1 | import CustomBpmnJsModeler, { CustomBpmnJsModelerOptions } from "./bpmn/CustomBpmnJsModeler";
2 | import CustomDmnJsModeler, { CustomDmnJsModelerOptions } from "./dmn/CustomDmnJsModeler";
3 |
4 | export {
5 | CustomBpmnJsModeler,
6 | CustomDmnJsModeler
7 | };
8 |
9 | export type {
10 | CustomBpmnJsModelerOptions,
11 | CustomDmnJsModelerOptions
12 | };
13 |
--------------------------------------------------------------------------------
/src/components/SvgIcon.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { tss } from "tss-react";
3 |
4 | interface Props {
5 | path: string;
6 | className?: string;
7 | }
8 |
9 | const useStyles = tss.create(() => ({
10 | root: {
11 | height: "1.5rem",
12 | width: "1.5rem",
13 | },
14 | }));
15 |
16 | const SvgIcon: React.FC = props => {
17 | const { classes, cx } = useStyles();
18 |
19 | return (
20 |
21 |
22 |
23 | );
24 | };
25 |
26 | export default SvgIcon;
27 |
--------------------------------------------------------------------------------
/src/components/ToggleGroup.tsx:
--------------------------------------------------------------------------------
1 | import React, { ReactNode } from "react";
2 | import { tss } from "tss-react";
3 |
4 | export interface ToggleOption {
5 | id: string;
6 | node: ReactNode;
7 | }
8 |
9 | interface Props {
10 | options: ToggleOption[];
11 | onChange: (id: string) => Promise;
12 | active: string;
13 | className?: string;
14 | }
15 |
16 | const useStyles = tss.create(() => ({
17 | root: {
18 | height: "40px",
19 | border: "1px solid #AAA",
20 | borderRadius: "4px",
21 | display: "flex",
22 | "&>*": {
23 | padding: "0px 16px !important",
24 | minWidth: "50px",
25 | border: "none",
26 | cursor: "pointer",
27 | transition: "all 400ms",
28 | color: "rgba(0, 0, 0, 0.54)",
29 | fill: "rgba(0, 0, 0, 0.54)",
30 | "&:hover": {
31 | backgroundColor: "rgba(0, 0, 0, 0.2)",
32 | color: "rgba(0, 0, 0, 0.87)",
33 | fill: "rgba(0, 0, 0, 0.87)",
34 | },
35 | },
36 | "&>:first-of-type": {
37 | borderTopLeftRadius: "4px",
38 | borderBottomLeftRadius: "4px",
39 | },
40 | "&>:last-child": {
41 | borderTopRightRadius: "4px",
42 | borderBottomRightRadius: "4px",
43 | },
44 | },
45 | active: {
46 | backgroundColor: "rgba(0, 0, 0, 0.15)",
47 | color: "rgba(0, 0, 0, 0.87)",
48 | fill: "rgba(0, 0, 0, 0.87)",
49 | },
50 | }));
51 |
52 | const ToggleGroup: React.FC = props => {
53 | const { classes, cx } = useStyles();
54 |
55 | return (
56 |
57 | {props.options.map(option => (
58 | void props.onChange(option.id)}
63 | >
64 | {option.node}
65 |
66 | ))}
67 |
68 | );
69 | };
70 |
71 | export default ToggleGroup;
72 |
--------------------------------------------------------------------------------
/src/editor/BpmnEditor.tsx:
--------------------------------------------------------------------------------
1 | import React, {
2 | MutableRefObject,
3 | ReactNode,
4 | useCallback,
5 | useEffect,
6 | useRef,
7 | useState,
8 | } from "react";
9 | import { Panel, PanelGroup, PanelResizeHandle } from "react-resizable-panels";
10 | import { tss } from "tss-react";
11 |
12 | import CustomBpmnJsModeler from "../bpmnio/bpmn/CustomBpmnJsModeler";
13 | import { createBpmnIoEvent } from "../events/bpmnio/BpmnIoEvents";
14 | import { Event } from "../events";
15 | import { createContentSavedEvent } from "../events/modeler/ContentSavedEvent";
16 | import { createNotificationEvent } from "../events/modeler/NotificationEvent";
17 | import { createPropertiesPanelResizedEvent } from "../events/modeler/PropertiesPanelResizedEvent";
18 | import { createUIUpdateRequiredEvent } from "../events/modeler/UIUpdateRequiredEvent";
19 |
20 | /**
21 | * The events that trigger a UI update required event.
22 | */
23 | const UI_UPDATE_REQUIRED_EVENTS = [
24 | "import.done",
25 | "saveXML.done",
26 | "commandStack.changed",
27 | "selection.changed",
28 | "attach",
29 | "elements.copied",
30 | "propertiesPanel.focusin",
31 | "propertiesPanel.focusout",
32 | "directEditing.activate",
33 | "directEditing.deactivate",
34 | "searchPad.closed",
35 | "searchPad.opened",
36 | ];
37 |
38 | /**
39 | * The events that trigger a content saved event.
40 | */
41 | const CONTENT_SAVED_EVENT = ["import.done", "commandStack.changed"];
42 |
43 | export interface BpmnPropertiesPanelOptions {
44 | /**
45 | * This option disables the properties panel.
46 | * CAUTION: Element templates will not be imported either if this option is set!
47 | */
48 | hidden?: boolean;
49 |
50 | /**
51 | * The initial, minimum, and maximum sizes of the properties panel.
52 | * Can be in % or px each.
53 | */
54 | size?: {
55 | // Default "25"
56 | initial?: number;
57 | // Default "5"
58 | min?: number;
59 | // Default "95"
60 | max?: number;
61 | };
62 |
63 | /**
64 | * The container to host the properties panel. By default, a styled div is created. If you
65 | * pass this option, make sure you set an ID and pass it via the `containerId` prop.
66 | * Pass `false` to prevent the rendering of this component.
67 | */
68 | container?: ReactNode;
69 |
70 | /**
71 | * The ID of the container to host the properties panel. Only required if you want to
72 | * use your own container.
73 | */
74 | containerId?: string;
75 |
76 | /**
77 | * The class name applied to the host of the properties panel.
78 | */
79 | className?: string;
80 |
81 | /**
82 | * The element templates to import into the modeler.
83 | */
84 | elementTemplates?: any[];
85 | }
86 |
87 | export interface BpmnModelerOptions {
88 | /**
89 | * Will receive the reference to the modeler instance.
90 | */
91 | refs?: MutableRefObject[];
92 |
93 | /**
94 | * The initial, minimum, and maximum sizes of the modeler panel.
95 | * Can be in % or px each.
96 | */
97 | size?: {
98 | // Default "75"
99 | initial?: number;
100 | // Default "5"
101 | min?: number;
102 | // Default "95"
103 | max?: number;
104 | };
105 |
106 | /**
107 | * The container to host the modeler. By default, a styled div is created. If you pass
108 | * this option, make sure you set an ID and pass it via the `containerId` prop.
109 | * Pass `false` to prevent the rendering of this component.
110 | */
111 | container?: ReactNode;
112 |
113 | /**
114 | * The ID of the container to host the modeler. Only required if you want to use your own
115 | * container.
116 | */
117 | containerId?: string;
118 |
119 | /**
120 | * The class name applied to the host of the modeler.
121 | */
122 | className?: string;
123 | }
124 |
125 | export interface BpmnEditorProps {
126 | /**
127 | * The class name applied to the root element.
128 | */
129 | className?: string;
130 |
131 | /**
132 | * The XML to display in the editor.
133 | */
134 | xml: string;
135 |
136 | /**
137 | * Whether this editor is currently active and visible to the user.
138 | */
139 | active: boolean;
140 |
141 | /**
142 | * Called whenever an event occurs.
143 | */
144 | onEvent: (event: Event) => void;
145 |
146 | /**
147 | * The options passed to the bpmn-js modeler.
148 | *
149 | * CAUTION: When this option object is changed, the old editor instance will be destroyed
150 | * and a new one will be created without automatic saving!
151 | */
152 | bpmnJsOptions?: any;
153 |
154 | /**
155 | * The options to control the appearance of the properties panel.
156 | *
157 | * CAUTION: When this option object is changed, the old editor instance will be destroyed
158 | * and a new one will be created without automatic saving!
159 | */
160 | propertiesPanelOptions?: BpmnPropertiesPanelOptions;
161 |
162 | /**
163 | * The options to control the appearance of the modeler.
164 | *
165 | * CAUTION: When this option object is changed, the old editor instance will be destroyed
166 | * and a new one will be created without automatic saving!
167 | */
168 | modelerOptions?: BpmnModelerOptions;
169 | }
170 |
171 | const useStyles = tss.create(() => ({
172 | modeler: {
173 | height: "100%",
174 | },
175 | propertiesPanel: {
176 | height: "100%",
177 | "&>div": {
178 | height: "100%",
179 | overflow: "auto",
180 | },
181 | },
182 | hidden: {
183 | display: "none",
184 | },
185 | modelerOnly: {
186 | height: "100%",
187 | },
188 | }));
189 |
190 | const BpmnEditor: React.FC = props => {
191 | const { classes, cx } = useStyles();
192 |
193 | const {
194 | active,
195 | xml,
196 | onEvent,
197 | className,
198 | bpmnJsOptions,
199 | modelerOptions,
200 | propertiesPanelOptions,
201 | } = props;
202 |
203 | const [initializeCount, setInitializeCount] = useState(0);
204 | const ref = useRef(undefined);
205 |
206 | const handleEvent = useCallback(
207 | (event: string, data: any) => {
208 | // TODO: Should bpmn-js events only be forwarded if the editor is currently active?
209 | onEvent(createBpmnIoEvent(event, data));
210 |
211 | if (!active) {
212 | return;
213 | }
214 |
215 | if (event === "elementTemplates.errors") {
216 | onEvent(
217 | createNotificationEvent(
218 | "Importing element templates failed. Check console for details.",
219 | "error",
220 | ),
221 | );
222 | console.error("Importing element templates failed.", data);
223 | }
224 |
225 | /**
226 | * If the event should trigger a UI update required event, do it.
227 | */
228 | if (event && UI_UPDATE_REQUIRED_EVENTS.includes(event)) {
229 | onEvent(createUIUpdateRequiredEvent(active));
230 | }
231 |
232 | /**
233 | * If the event should trigger a content saved event, do it.
234 | */
235 | if (event && CONTENT_SAVED_EVENT.includes(event) && ref.current) {
236 | ref.current
237 | .save()
238 | .then(saved => {
239 | onEvent(
240 | createContentSavedEvent(
241 | saved.xml,
242 | saved.svg,
243 | "diagram.changed",
244 | ),
245 | );
246 | })
247 | .catch(e => {
248 | console.warn("Could not save document", e);
249 | });
250 | }
251 | },
252 | [active, onEvent],
253 | );
254 |
255 | /**
256 | * Instantiates the modeler and properties panel. Only happens once on mount.
257 | */
258 | useEffect(() => {
259 | const modeler = new CustomBpmnJsModeler({
260 | container: modelerOptions?.containerId ?? "#bpmnview",
261 | propertiesPanel: propertiesPanelOptions?.hidden
262 | ? undefined
263 | : (propertiesPanelOptions?.containerId ?? "#bpmnprop"),
264 | bpmnJsOptions: bpmnJsOptions,
265 | });
266 |
267 | ref.current = modeler;
268 | if (modelerOptions?.refs) {
269 | modelerOptions.refs.forEach(r => {
270 | r.current = modeler;
271 | });
272 | }
273 |
274 | setInitializeCount(count => count + 1);
275 |
276 | return () => {
277 | modeler.unregisterGlobalEventListener(handleEvent);
278 | modeler.destroy();
279 | ref.current = undefined;
280 | if (modelerOptions?.refs) {
281 | modelerOptions.refs.forEach(r => {
282 | r.current = undefined;
283 | });
284 | }
285 | };
286 | }, [
287 | handleEvent,
288 | bpmnJsOptions,
289 | propertiesPanelOptions,
290 | modelerOptions?.containerId,
291 | ]);
292 |
293 | /**
294 | * Imports the specified XML. The following steps are executed:
295 | *
296 | * 1. Export the currently loaded XML.
297 | * 2. Check if it is different from the specified XML.
298 | * 3. Import the specified XML if it has changed.
299 | * 4. Show any errors or warnings that occurred during import.
300 | */
301 | const importXml = useCallback(
302 | async (newXml: string) => {
303 | if (ref.current) {
304 | try {
305 | const currentXml = await ref.current?.saveXML({
306 | format: true,
307 | preamble: false,
308 | });
309 |
310 | if (newXml === currentXml.xml) {
311 | // XML has not changed
312 | return;
313 | }
314 | // eslint-disable-next-line @typescript-eslint/no-unused-vars
315 | } catch (e) {
316 | // The editor has not yet loaded any content
317 | // => no definitions loaded, ignores the error
318 | }
319 |
320 | try {
321 | const result = ref.current.importXML(newXml);
322 | const count = result.warnings?.length ?? 0;
323 | if (count > 0) {
324 | console.log("Imported with warnings", result.warnings);
325 | onEvent(
326 | createNotificationEvent(
327 | `Imported with ${count} warning${count === 1 ? "" : "s"}. See console for details.`,
328 | "warning",
329 | ),
330 | );
331 | }
332 | } catch (e) {
333 | console.error("Could not import XML", e);
334 | onEvent(
335 | createNotificationEvent(
336 | "Could not import changed XML. Is it invalid? See console for details.",
337 | "error",
338 | ),
339 | );
340 | }
341 | }
342 | },
343 | [onEvent],
344 | );
345 |
346 | /**
347 | * Imports the document XML whenever it changes.
348 | */
349 | useEffect(() => {
350 | if (initializeCount > 0) {
351 | importXml(xml).catch(e => {
352 | console.error("Could not import XML", e);
353 | onEvent(
354 | createNotificationEvent(
355 | "Could not import changed XML. Is it invalid? See console for details.",
356 | "error",
357 | ),
358 | );
359 | });
360 | }
361 | }, [xml, importXml, initializeCount, onEvent]);
362 |
363 | useEffect(() => {
364 | const modeler = ref.current;
365 | if (initializeCount > 0 && modeler) {
366 | modeler.registerGlobalEventListener(handleEvent);
367 | return () => modeler.unregisterGlobalEventListener(handleEvent);
368 | }
369 | return undefined;
370 | }, [initializeCount, handleEvent]);
371 |
372 | /**
373 | * Imports the specified element templates whenever they change.
374 | */
375 | useEffect(() => {
376 | if (!propertiesPanelOptions?.hidden) {
377 | ref.current?.importElementTemplates(
378 | propertiesPanelOptions?.elementTemplates ?? [],
379 | );
380 | }
381 | }, [propertiesPanelOptions?.hidden, propertiesPanelOptions?.elementTemplates]);
382 |
383 | const onPropertiesPanelWidthChanged = useCallback(
384 | (sizes: number[]) => {
385 | onEvent(createPropertiesPanelResizedEvent(sizes[1]));
386 | },
387 | [onEvent],
388 | );
389 |
390 | const modelerContainer: ReactNode = modelerOptions?.container ?? (
391 |
392 | );
393 |
394 | const propertiesPanelContainer: ReactNode = propertiesPanelOptions?.container ?? (
395 |
399 | );
400 |
401 | if (propertiesPanelOptions?.hidden) {
402 | return (
403 | {modelerContainer}
404 | );
405 | }
406 |
407 | return (
408 |
416 |
421 | {modelerContainer}
422 |
423 |
430 |
435 | {propertiesPanelContainer}
436 |
437 |
438 | );
439 | };
440 |
441 | export default BpmnEditor;
442 |
--------------------------------------------------------------------------------
/src/editor/DmnEditor.tsx:
--------------------------------------------------------------------------------
1 | import React, {
2 | MutableRefObject,
3 | ReactNode,
4 | useCallback,
5 | useEffect,
6 | useRef,
7 | useState,
8 | } from "react";
9 | import { Panel, PanelGroup, PanelResizeHandle } from "react-resizable-panels";
10 |
11 | import CustomDmnJsModeler, { DmnView } from "../bpmnio/dmn/CustomDmnJsModeler";
12 | import { createBpmnIoEvent } from "../events/bpmnio/BpmnIoEvents";
13 | import { Event } from "../events";
14 | import { createContentSavedEvent } from "../events/modeler/ContentSavedEvent";
15 | import { createDmnViewsChangedEvent } from "../events/modeler/DmnViewsChangedEvent";
16 | import { createNotificationEvent } from "../events/modeler/NotificationEvent";
17 | import { createPropertiesPanelResizedEvent } from "../events/modeler/PropertiesPanelResizedEvent";
18 | import { createUIUpdateRequiredEvent } from "../events/modeler/UIUpdateRequiredEvent";
19 | import { tss } from "tss-react";
20 |
21 | /**
22 | * The events that trigger a UI update required event.
23 | */
24 | const UI_UPDATE_REQUIRED_EVENTS = [
25 | "import.done",
26 | "saveXML.done",
27 | "attach",
28 | "dmn.views.changed",
29 | "views.changed",
30 | "view.contentChanged",
31 | "view.selectionChanged",
32 | "view.directEditingChanged",
33 | "propertiesPanel.focusin",
34 | "propertiesPanel.focusout",
35 | ];
36 |
37 | /**
38 | * The events that trigger a content saved event.
39 | */
40 | const CONTENT_SAVED_EVENT = [
41 | "import.done",
42 | "view.contentChanged",
43 | "dmn.views.changed",
44 | "views.changed",
45 | "elements.changed",
46 | ];
47 |
48 | export interface DmnPropertiesPanelOptions {
49 | /**
50 | * This option disables the properties panel.
51 | */
52 | hidden?: boolean;
53 |
54 | /**
55 | * The initial, minimum, and maximum sizes of the properties panel.
56 | * Can be in % or px each.
57 | */
58 | size?: {
59 | // Default "25"
60 | initial?: number;
61 | // Default "5"
62 | min?: number;
63 | // Default "95"
64 | max?: number;
65 | };
66 |
67 | /**
68 | * The container to host the properties panel. By default, a styled div is created. If you
69 | * pass this option, make sure you set an ID and pass it via the `containerId` prop.
70 | * Pass `false` to prevent the rendering of this component.
71 | */
72 | container?: ReactNode;
73 |
74 | /**
75 | * The ID of the container to host the properties panel. Only required if you want to
76 | * use your own container.
77 | */
78 | containerId?: string;
79 |
80 | /**
81 | * The class name applied to the host of the properties panel.
82 | */
83 | className?: string;
84 | }
85 |
86 | export interface DmnModelerOptions {
87 | /**
88 | * Will receive the reference to the modeler instance.
89 | */
90 | refs?: MutableRefObject[];
91 |
92 | /**
93 | * The initial, minimum, and maximum sizes of the modeler panel.
94 | * Can be in % or px each.
95 | */
96 | size?: {
97 | // Default "75"
98 | initial?: number;
99 | // Default "95"
100 | min?: number;
101 | // Default "5"
102 | max?: number;
103 | };
104 |
105 | /**
106 | * The container to host the modeler. By default, a styled div is created. If you pass
107 | * this option, make sure you set an ID and pass it via the `containerId` prop.
108 | * Pass `false` to prevent the rendering of this component.
109 | */
110 | container?: ReactNode;
111 |
112 | /**
113 | * The ID of the container to host the modeler. Only required if you want to use your own
114 | * container.
115 | */
116 | containerId?: string;
117 |
118 | /**
119 | * The class name applied to the host of the modeler.
120 | */
121 | className?: string;
122 | }
123 |
124 | export interface DmnEditorProps {
125 | /**
126 | * The XML to display in the editor.
127 | */
128 | xml: string;
129 |
130 | /**
131 | * Whether this editor is currently active and visible to the user.
132 | */
133 | active: boolean;
134 |
135 | /**
136 | * Called whenever an event occurs.
137 | */
138 | onEvent: (event: Event) => void;
139 |
140 | /**
141 | * The class name applied to the host of the properties panel.
142 | */
143 | className?: string;
144 |
145 | /**
146 | * The options passed to the dmn-js modeler.
147 | *
148 | * CAUTION: When this option object is changed, the old editor instance will be destroyed
149 | * and a new one will be created without automatic saving!
150 | */
151 | dmnJsOptions?: any;
152 |
153 | /**
154 | * The options to control the appearance of the properties panel.
155 | *
156 | * CAUTION: When this option object is changed, the old editor instance will be destroyed
157 | * and a new one will be created without automatic saving!
158 | */
159 | propertiesPanelOptions?: DmnPropertiesPanelOptions;
160 |
161 | /**
162 | * The options to control the appearance of the modeler.
163 | *
164 | * CAUTION: When this option object is changed, the old editor instance will be destroyed
165 | * and a new one will be created without automatic saving!
166 | */
167 | modelerOptions?: DmnModelerOptions;
168 | }
169 |
170 | const useStyles = tss.create(() => ({
171 | modeler: {
172 | height: "100%",
173 | },
174 | propertiesPanel: {
175 | height: "100%",
176 | "&>div": {
177 | height: "100%",
178 | },
179 | },
180 | hidden: {
181 | display: "none",
182 | },
183 | modelerOnly: {
184 | height: "100%",
185 | },
186 | }));
187 |
188 | const DmnEditor: React.FC = props => {
189 | const { classes, cx } = useStyles();
190 |
191 | const {
192 | xml,
193 | active,
194 | onEvent,
195 | dmnJsOptions,
196 | propertiesPanelOptions,
197 | modelerOptions,
198 | className,
199 | } = props;
200 |
201 | const [activeView, setActiveView] = useState(undefined);
202 | const [initializeCount, setInitializeCount] = useState(0);
203 | const ref = useRef(null);
204 |
205 | const handleEvent = useCallback(
206 | (event: string, data: any) => {
207 | // TODO: Should dmn-js events only be forwarded if the editor is currently active?
208 | onEvent(createBpmnIoEvent(event, data));
209 |
210 | if (!active) {
211 | return;
212 | }
213 |
214 | if (event === "views.changed") {
215 | setActiveView(data.activeView);
216 | onEvent(createDmnViewsChangedEvent(data.views, data.activeView));
217 | }
218 |
219 | /**
220 | * If the event should trigger a UI update required event, do it.
221 | */
222 | if (event && UI_UPDATE_REQUIRED_EVENTS.includes(event)) {
223 | onEvent(createUIUpdateRequiredEvent(active));
224 | }
225 |
226 | /**
227 | * If the event should trigger a content saved event, do it.
228 | */
229 | if (event && CONTENT_SAVED_EVENT.includes(event) && ref.current) {
230 | ref.current
231 | .save({ format: true })
232 | .then(saved => {
233 | // TODO: Save SVG (but which viewer?)
234 | onEvent(
235 | createContentSavedEvent(
236 | saved.xml,
237 | undefined,
238 | "diagram.changed",
239 | ),
240 | );
241 | })
242 | .catch(e => {
243 | console.warn("Could not save document", e);
244 | });
245 | }
246 | },
247 | [active, onEvent],
248 | );
249 |
250 | const viewsChangedCallback = useCallback(
251 | (event: any, data: any) => {
252 | void handleEvent(event.type, data);
253 | if (ref.current?.getActiveViewer()) {
254 | void ref.current?.registerGlobalEventListener(handleEvent);
255 | return () => ref.current?.unregisterGlobalEventListener(handleEvent);
256 | }
257 | return undefined;
258 | },
259 | [handleEvent],
260 | );
261 |
262 | /**
263 | * Instantiates the modeler and properties panel. Only happens once on mount.
264 | */
265 | useEffect(() => {
266 | const modeler = new CustomDmnJsModeler({
267 | container: modelerOptions?.containerId ?? "#dmnview",
268 | propertiesPanel: propertiesPanelOptions?.hidden
269 | ? undefined
270 | : (propertiesPanelOptions?.containerId ?? "#dmnprop"),
271 | dmnJsOptions: dmnJsOptions,
272 | });
273 |
274 | ref.current = modeler;
275 | if (modelerOptions?.refs) {
276 | modelerOptions.refs.forEach(r => {
277 | r.current = modeler;
278 | });
279 | }
280 |
281 | setInitializeCount(cur => cur + 1);
282 |
283 | return () => {
284 | if (modelerOptions?.refs) {
285 | modelerOptions.refs.forEach(r => {
286 | r.current = undefined;
287 | });
288 | }
289 | modeler.destroy();
290 | };
291 | }, [dmnJsOptions, modelerOptions?.containerId, propertiesPanelOptions]);
292 |
293 | useEffect(() => {
294 | const modeler = ref.current;
295 | modeler?.on("views.changed", viewsChangedCallback);
296 | return () => modeler?.off("views.changed", viewsChangedCallback);
297 | }, [viewsChangedCallback]);
298 |
299 | /**
300 | * Imports the specified XML. The following steps are executed:
301 | *
302 | * 1. Export the currently loaded XML.
303 | * 2. Check if it is different from the specified XML.
304 | * 3. Import the specified XML if it has changed.
305 | * 4. Show any errors or warnings that occurred during import.
306 | */
307 | const importXml = useCallback(
308 | async (newXml: string, open = false): Promise => {
309 | if (ref.current) {
310 | try {
311 | const currentXml = await ref.current?.save({
312 | format: true,
313 | });
314 |
315 | if (newXml === currentXml.xml) {
316 | // XML has not changed
317 | return;
318 | }
319 | // eslint-disable-next-line @typescript-eslint/no-unused-vars
320 | } catch (e) {
321 | // The editor has not yet loaded any content
322 | // ⇒ no definitions loaded, ignore the error
323 | }
324 |
325 | try {
326 | const result = await ref.current.import(newXml, open);
327 | const count = result.warnings.length;
328 | if (count > 0) {
329 | console.log("Imported with warnings", result.warnings);
330 | onEvent(
331 | createNotificationEvent(
332 | `Imported with ${count} warning${count === 1 ? "" : "s"}. See console for details.`,
333 | "warning",
334 | ),
335 | );
336 | }
337 | } catch (e) {
338 | console.error("Could not import XML", e);
339 | onEvent(
340 | createNotificationEvent(
341 | "Could not import changed XML. Is it invalid? See console for details.",
342 | "error",
343 | ),
344 | );
345 | }
346 | }
347 | },
348 | [onEvent],
349 | );
350 |
351 | /**
352 | * Imports the document XML whenever it changes.
353 | */
354 | useEffect(() => {
355 | if (initializeCount > 0) {
356 | // Only open the view on first render
357 | void importXml(xml, initializeCount === 1);
358 | }
359 | }, [xml, importXml, initializeCount]);
360 |
361 | const onPropertiesPanelWidthChanged = useCallback(
362 | (sizes: number[]) => {
363 | onEvent(createPropertiesPanelResizedEvent(sizes[1]));
364 | },
365 | [onEvent],
366 | );
367 |
368 | const modelerContainer: ReactNode = modelerOptions?.container ?? (
369 |
370 | );
371 |
372 | const propertiesPanelContainer: ReactNode = propertiesPanelOptions?.container ?? (
373 |
377 | );
378 |
379 | if (propertiesPanelOptions?.hidden) {
380 | return (
381 | {modelerContainer}
382 | );
383 | }
384 |
385 | return (
386 |
394 |
399 | {modelerContainer}
400 |
401 |
408 |
413 | {propertiesPanelContainer}
414 |
415 |
416 | );
417 | };
418 |
419 | export default DmnEditor;
420 |
--------------------------------------------------------------------------------
/src/editor/XmlEditor.tsx:
--------------------------------------------------------------------------------
1 | import React, {
2 | MutableRefObject,
3 | useCallback,
4 | useEffect,
5 | useMemo,
6 | useState,
7 | } from "react";
8 | import deepmerge from "deepmerge";
9 | import Editor, { EditorProps, loader } from "@monaco-editor/react";
10 | import * as monaco from "monaco-editor";
11 | import { tss } from "tss-react";
12 |
13 | loader.config({ monaco });
14 |
15 | export interface MonacoOptions {
16 | /**
17 | * Will receive the reference to the editor instance, the monaco instance, and the container
18 | * element.
19 | */
20 | refs?: MutableRefObject[];
21 |
22 | /**
23 | * Additional props to pass to the editor component. This will override the defaults defined by
24 | * this component.
25 | */
26 | props?: Partial;
27 |
28 | /**
29 | * Additional options to pass to the editor component. This will override the defaults defined
30 | * by this component.
31 | */
32 | options?: Partial;
33 | }
34 |
35 | export interface XmlEditorProps {
36 | /**
37 | * The XML to display in the editor.
38 | */
39 | xml: string;
40 |
41 | /**
42 | * Whether this editor is currently active and visible to the user.
43 | */
44 | active: boolean;
45 |
46 | /**
47 | * Callback to execute whenever the diagram's XML changes.
48 | *
49 | * @param xml The new XML
50 | */
51 | onChanged: (xml: string) => void;
52 |
53 | /**
54 | * The options to pass to the monaco editor.
55 | */
56 | monacoOptions?: MonacoOptions;
57 |
58 | /**
59 | * The class name applied to the host of the modeler.
60 | */
61 | className?: string;
62 | }
63 |
64 | const useStyles = tss.create(() => ({
65 | root: {
66 | height: "100%",
67 | "&>div": {
68 | height: "100%",
69 | overflow: "hidden",
70 | },
71 | },
72 | hidden: {
73 | display: "none",
74 | },
75 | }));
76 |
77 | const XmlEditor: React.FC = props => {
78 | const { classes, cx } = useStyles();
79 |
80 | const { xml, onChanged, active, monacoOptions, className } = props;
81 |
82 | const [xmlEditorShown, setXmlEditorShown] = useState(false);
83 |
84 | const onEditorMount = useCallback(
85 | (editor: monaco.editor.IStandaloneCodeEditor) => {
86 | monacoOptions?.refs?.forEach(e => {
87 | e.current = editor;
88 | });
89 | },
90 | [monacoOptions],
91 | );
92 |
93 | const onXmlChanged = useCallback(
94 | (value?: string) => {
95 | if (active) {
96 | const xmlValue = value ?? xml; // value is empty when the editor initialized
97 | onChanged(xmlValue);
98 | }
99 | },
100 | [xml, active, onChanged],
101 | );
102 |
103 | /**
104 | * Initializes the editor when it is visible for the first time. If it is shown when it is
105 | * first mounted, the size is wrong.
106 | */
107 | useEffect(() => {
108 | if (active && !xmlEditorShown) {
109 | setXmlEditorShown(true);
110 | }
111 | }, [xmlEditorShown, active]);
112 |
113 | const options = useMemo(
114 | () =>
115 | deepmerge(
116 | {
117 | theme: "vs-light",
118 | wordWrap: "on",
119 | wrappingIndent: "deepIndent",
120 | scrollBeyondLastLine: false,
121 | minimap: {
122 | enabled: false,
123 | },
124 | },
125 | monacoOptions?.options ?? {},
126 | ),
127 | [monacoOptions?.options],
128 | );
129 |
130 | /**
131 | * Only show the editor once it has become active or the editor size will be wrong.
132 | */
133 | if (!xmlEditorShown) {
134 | return null;
135 | }
136 |
137 | return (
138 |
139 |
149 |
150 | );
151 | };
152 |
153 | export default React.memo(XmlEditor);
154 |
--------------------------------------------------------------------------------
/src/events/Events.ts:
--------------------------------------------------------------------------------
1 | export type ModelerEventType =
2 | | "content.saved"
3 | | "ui.update.required"
4 | | "notification"
5 | | "properties.panel.resized"
6 | | "dmn.views.changed";
7 |
8 | type EventSource =
9 | | "bpmnio"
10 | | "modeler";
11 |
12 | export interface Event {
13 | source: Source;
14 | event: Source extends "modeler" ? ModelerEventType : string;
15 | data: Data;
16 | }
17 |
--------------------------------------------------------------------------------
/src/events/bpmnio/BpmnIoEvents.ts:
--------------------------------------------------------------------------------
1 | import { Event } from "../Events";
2 |
3 | export const createBpmnIoEvent = (event: string, data: any): Event => ({
4 | source: "bpmnio",
5 | event: event,
6 | data: data,
7 | });
8 |
9 | export const isBpmnIoEvent = (event: Event): event is Event =>
10 | event.source === "bpmnio";
11 |
--------------------------------------------------------------------------------
/src/events/index.ts:
--------------------------------------------------------------------------------
1 | import { isBpmnIoEvent } from "./bpmnio/BpmnIoEvents";
2 | import { Event } from "./Events";
3 | import { ContentSavedReason, isContentSavedEvent } from "./modeler/ContentSavedEvent";
4 | import { isDmnViewsChangedEvent } from "./modeler/DmnViewsChangedEvent";
5 | import { isNotificationEvent } from "./modeler/NotificationEvent";
6 | import { isPropertiesPanelResizedEvent } from "./modeler/PropertiesPanelResizedEvent";
7 | import { isUIUpdateRequiredEvent } from "./modeler/UIUpdateRequiredEvent";
8 |
9 | export {
10 | isBpmnIoEvent,
11 | isUIUpdateRequiredEvent,
12 | isPropertiesPanelResizedEvent,
13 | isNotificationEvent,
14 | isDmnViewsChangedEvent,
15 | isContentSavedEvent
16 | };
17 |
18 | export type {
19 | Event,
20 | ContentSavedReason
21 | };
22 |
--------------------------------------------------------------------------------
/src/events/modeler/ContentSavedEvent.ts:
--------------------------------------------------------------------------------
1 | import { Event } from "../Events";
2 |
3 | const EventName = "content.saved";
4 |
5 | export type ContentSavedReason =
6 | /**
7 | * The diagram inside the bpmnjs / dmnjs editor has been changed by the user.
8 | */
9 | | "diagram.changed"
10 |
11 | /**
12 | * The user has changed the XML inside the XML editor.
13 | */
14 | | "xml.changed"
15 |
16 | /**
17 | * The view has been changed, e.g., from the bpmnjs editor to the XML editor or the other way.
18 | */
19 | | "view.changed";
20 |
21 | /**
22 | * Indicates that the content of the model has changed for some reason. The reasons are documented
23 | * above.
24 | */
25 | export interface ContentSavedEventData {
26 | /**
27 | * The new XML model.
28 | */
29 | xml: string;
30 |
31 | /**
32 | * The new SVG model. Only filled if the reason for the change is the bpmnjs / dmnjs editor.
33 | */
34 | svg: string | undefined;
35 |
36 | /**
37 | * The reason for the change.
38 | */
39 | reason: ContentSavedReason;
40 | }
41 |
42 | export const createContentSavedEvent = (
43 | xml: string,
44 | svg: string | undefined,
45 | reason: ContentSavedReason,
46 | ): Event => ({
47 | source: "modeler",
48 | event: EventName,
49 | data: {
50 | xml,
51 | svg,
52 | reason,
53 | },
54 | });
55 |
56 | export const isContentSavedEvent = (
57 | event: Event,
58 | ): event is Event =>
59 | event.source === "modeler" && event.event === EventName;
60 |
--------------------------------------------------------------------------------
/src/events/modeler/DmnViewsChangedEvent.ts:
--------------------------------------------------------------------------------
1 | import { DmnView } from "../../bpmnio/dmn/CustomDmnJsModeler";
2 | import type { Event } from "../Events";
3 |
4 | const EventName = "dmn.views.changed";
5 |
6 | /**
7 | * Indicates that the views available or the selected view in the DMN editor have changed.
8 | */
9 | export interface DmnViewsChangedEventData {
10 | /**
11 | * The list of all available views.
12 | */
13 | views: DmnView[];
14 |
15 | /**
16 | * The currently active view.
17 | */
18 | activeView: DmnView | undefined;
19 | }
20 |
21 | export const createDmnViewsChangedEvent = (
22 | views: DmnView[],
23 | activeView: DmnView | undefined
24 | ): Event => ({
25 | source: "modeler",
26 | event: EventName,
27 | data: {
28 | views,
29 | activeView
30 | }
31 | });
32 |
33 | export const isDmnViewsChangedEvent = (
34 | event: Event
35 | ): event is Event => (
36 | event.source === "modeler" && event.event === EventName
37 | );
38 |
--------------------------------------------------------------------------------
/src/events/modeler/NotificationEvent.ts:
--------------------------------------------------------------------------------
1 | import type { Event } from "../Events";
2 |
3 | const EventName = "notification";
4 |
5 | export type NotificationSeverity =
6 | | "success"
7 | | "info"
8 | | "warning"
9 | | "error";
10 |
11 | /**
12 | * Indicates any notification, could be a success, info, warning, or failure. This may be displayed
13 | * by the application directly to the user, if desired.
14 | */
15 | export interface NotificationEventData {
16 | /**
17 | * The message text. A human readable string.
18 | */
19 | message: string;
20 |
21 | /**
22 | * The severity of the message.
23 | */
24 | severity: NotificationSeverity;
25 | }
26 |
27 | export const createNotificationEvent = (
28 | message: string,
29 | severity: NotificationSeverity
30 | ): Event => ({
31 | source: "modeler",
32 | event: EventName,
33 | data: {
34 | message,
35 | severity
36 | }
37 | });
38 |
39 | export const isNotificationEvent = (event: Event): event is Event => (
40 | event.source === "modeler" && event.event === EventName
41 | );
42 |
--------------------------------------------------------------------------------
/src/events/modeler/PropertiesPanelResizedEvent.ts:
--------------------------------------------------------------------------------
1 | import { Event } from "../Events";
2 |
3 | const EventName = "properties.panel.resized";
4 |
5 | /**
6 | * Indicates that the width of the properties panel has changed.
7 | */
8 | export interface PropertiesPanelResizedEventData {
9 | /**
10 | * The new width of the properties panel in px.
11 | */
12 | width: number;
13 | }
14 |
15 | export const createPropertiesPanelResizedEvent = (
16 | width: number
17 | ): Event => ({
18 | source: "modeler",
19 | event: EventName,
20 | data: { width }
21 | });
22 |
23 | export const isPropertiesPanelResizedEvent = (
24 | event: Event
25 | ): event is Event => (
26 | event.source === "modeler" && event.event === EventName
27 | );
28 |
--------------------------------------------------------------------------------
/src/events/modeler/UIUpdateRequiredEvent.ts:
--------------------------------------------------------------------------------
1 | import { Event } from "../Events";
2 |
3 | const EventName = "ui.update.required";
4 |
5 | /**
6 | * Indicates that something in the modeler has changed so that external UI that depends on modeler
7 | * state, such as button bars or menus, have to be updated. This event is only triggered for the
8 | * bpmnjs and dmnjs editors, but not for the XML editor.
9 | */
10 | export interface UIUpdateRequiredEventData {
11 | /**
12 | * Whether the modeler instance that sent the event is currently active.
13 | */
14 | isActive: boolean;
15 | }
16 |
17 | export const createUIUpdateRequiredEvent = (
18 | isActive: boolean
19 | ): Event => ({
20 | source: "modeler",
21 | event: EventName,
22 | data: { isActive }
23 | });
24 |
25 | export const isUIUpdateRequiredEvent = (
26 | event: Event
27 | ): event is Event => (
28 | event.source === "modeler" && event.event === EventName
29 | );
30 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import BpmnModeler from "./BpmnModeler";
2 | import DmnModeler from "./DmnModeler";
3 |
4 | export {
5 | BpmnModeler,
6 | DmnModeler
7 | };
8 |
9 | export * from "./events";
10 | export * from "./bpmnio";
11 |
--------------------------------------------------------------------------------
/src/types/bpmn-io.d.ts:
--------------------------------------------------------------------------------
1 | declare module "@bpmn-io/properties-panel";
2 |
--------------------------------------------------------------------------------
/src/types/bpmn-js-element-templates.ts:
--------------------------------------------------------------------------------
1 | declare module "bpmn-js-element-templates";
--------------------------------------------------------------------------------
/src/types/bpmn-js-properties-panel.d.ts:
--------------------------------------------------------------------------------
1 | declare module "bpmn-js-properties-panel";
2 | declare module "@bpmn-io/element-template-chooser";
3 |
--------------------------------------------------------------------------------
/src/types/bpmn-js.d.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/explicit-module-boundary-types */
2 |
3 | declare module "bpmn-js/lib/Modeler" {
4 | class Modeler {
5 | constructor(options: any);
6 |
7 | /**
8 | * Returns the requested component.
9 | *
10 | * @param component The name of the component to return
11 | */
12 | get(component: string): any;
13 |
14 | /**
15 | * Imports the XML into the editor.
16 | *
17 | * @param xml The XML to import
18 | * @return List of import warnings
19 | * @throws If the import failed
20 | */
21 | importXML(xml: string): ImportResponse;
22 |
23 | /**
24 | * Saves the editor content as XML and returns it.
25 | *
26 | * @param format Whether to format the output
27 | * @param preamble Whether to include the preamble ``
28 | * @return The editor content as XML
29 | */
30 | saveXML({ format = false, preamble = false }): Promise<{ xml: string }>;
31 |
32 | /**
33 | * Saves the editor content as SVG and returns it.
34 | *
35 | * @return The editor content as SVG
36 | */
37 | saveSVG(): Promise<{ svg: string }>;
38 |
39 | /**
40 | * Registers an event listener for bpmn-js.
41 | *
42 | * @param event The name of the event
43 | * @param handler The listener to register
44 | */
45 | on(event: string, handler: (event: any & { type: string; }, data: any) => void);
46 |
47 | /**
48 | * Unregisters a previously registered listener for bpmn-js.
49 | *
50 | * @param event The name of the event
51 | * @param handler The previously registered listener to unregister
52 | */
53 | off(event: string, handler: (event: any & { type: string; }, data: any) => void);
54 |
55 | /**
56 | * Destroys the modeler instance.
57 | */
58 | destroy();
59 | }
60 |
61 | export default Modeler;
62 | }
63 |
64 | interface ImportResponse {
65 | warnings: ImportWarning[];
66 | }
67 |
68 | interface ImportWarning {
69 | error: Error;
70 | message: string;
71 | }
72 |
--------------------------------------------------------------------------------
/src/types/diagram-js-origin.d.ts:
--------------------------------------------------------------------------------
1 | declare module "diagram-js-origin";
2 |
--------------------------------------------------------------------------------
/src/types/dmn-js-properties-panel.d.ts:
--------------------------------------------------------------------------------
1 | declare module "dmn-js-properties-panel";
2 |
--------------------------------------------------------------------------------
/src/types/dmn-js.d.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/explicit-module-boundary-types */
2 |
3 | declare module "dmn-js/lib/Modeler" {
4 | import { DmnView, DmnViewer } from "../bpmnio/dmn/CustomDmnJsModeler";
5 |
6 | class Modeler {
7 | constructor(options: any);
8 |
9 | get(param: any): any;
10 |
11 | /**
12 | * Imports the passed XML into the editor.
13 | *
14 | * @param xml The XML to import
15 | * @param options Options for customization
16 | * open: Whether to open the diagram after importing
17 | */
18 | importXML(xml: string, options?: { open: boolean }): Promise;
19 |
20 | saveXML({ format: boolean }): Promise;
21 |
22 | /**
23 | * Registers an event listener for bpmn-js.
24 | *
25 | * @param event The name of the event
26 | * @param handler The listener to register
27 | */
28 | on(event: string, handler: (event: any, data: any) => void);
29 |
30 | /**
31 | * Unregisters a previously registered listener for bpmn-js.
32 | *
33 | * @param event The name of the event
34 | * @param handler The previously registered listener to unregister
35 | */
36 | off(event: string, handler: (event: any, data: any) => void);
37 |
38 | /**
39 | * Returns the active viewer.
40 | */
41 | getActiveViewer(): DmnViewer | undefined;
42 |
43 | /**
44 | * Returns all available views.
45 | */
46 | getViews(): DmnView[];
47 |
48 | /**
49 | * Returns the active view.
50 | */
51 | getActiveView(): DmnView | undefined;
52 |
53 | /**
54 | * Opens the specified view.
55 | *
56 | * @param view The view to open
57 | */
58 | open(view: DmnView): Promise;
59 |
60 | /**
61 | * Destroys the modeler instance.
62 | */
63 | destroy();
64 | }
65 |
66 | export type OpenResult = {
67 | /**
68 | * Warnings occurred during the opening.
69 | */
70 | warnings: string[];
71 | };
72 |
73 | export type OpenError = {
74 | error: Error;
75 | /**
76 | * Warnings occurred during the opening.
77 | */
78 | warnings: string[];
79 | };
80 |
81 | export type ImportXMLResult = {
82 | warnings: string[];
83 | };
84 |
85 | export type SaveXMLResult = {
86 | xml: string;
87 | };
88 |
89 | export default Modeler;
90 | }
91 |
--------------------------------------------------------------------------------
/static/screenshot.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Miragon/camunda-web-modeler/a7387a8dbf4f3aa0b1a35827732b53b8216520e8/static/screenshot.jpg
--------------------------------------------------------------------------------
/tsconfig.index.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "."
5 | },
6 | "include": [
7 | "./index.ts"
8 | ],
9 | "exclude": []
10 | }
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES5",
4 | "lib": [
5 | "dom",
6 | "dom.iterable",
7 | "esnext"
8 | ],
9 | "jsx": "react",
10 | "strict": true,
11 | "outDir": "dist",
12 | "module": "ESNext",
13 | "declaration": true,
14 | "skipLibCheck": true,
15 | "removeComments": false,
16 | "esModuleInterop": true,
17 | "isolatedModules": true,
18 | "resolveJsonModule": true,
19 | "moduleResolution": "node",
20 | "noFallthroughCasesInSwitch": true,
21 | "allowSyntheticDefaultImports": true,
22 | "forceConsistentCasingInFileNames": true
23 | },
24 | "include": [
25 | "src/**/*"
26 | ]
27 | }
28 |
--------------------------------------------------------------------------------