├── .eslintignore
├── .eslintrc
├── .gitignore
├── CHANGELOG.md
├── LICENSE
├── README.md
├── assets
└── comments.css
├── docs
└── screenshot.png
├── karma.conf.js
├── lib
├── comments.js
├── index.js
└── util.js
├── package-lock.json
├── package.json
└── test
├── .eslintrc
├── assets
└── comments-test.css
├── spec
├── commentsSpec.js
├── simple-with-comments.bpmn
└── simple.bpmn
├── suite.js
└── test-helper.js
/.eslintignore:
--------------------------------------------------------------------------------
1 | dist
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "plugin:bpmn-io/browser"
3 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | dist/
2 | node_modules/
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | All notable changes to [bpmn-js-embedded-comments](https://github.com/bpmn-io/bpmn-js-embedded-comments) are documented here. We use [semantic versioning](http://semver.org/) for releases.
4 |
5 |
6 | ## Unreleased
7 |
8 | ___Note:__ Yet to be released changes appear here._
9 |
10 | ## 0.7.0
11 |
12 | * `FEAT`: make extension translatable ([#8](https://github.com/bpmn-io/bpmn-js-embedded-comments/issues/8))
13 |
14 | ## ...
15 |
16 | Check `git log` for earlier releases.
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2014 Nico Rehwaldt
4 | Copyright (c) 2015-present Camunda Services GmbH
5 |
6 | Permission is hereby granted, free of charge, to any person obtaining a copy
7 | of this software and associated documentation files (the "Software"), to deal
8 | in the Software without restriction, including without limitation the rights
9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | copies of the Software, and to permit persons to whom the Software is
11 | furnished to do so, subject to the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be included in
14 | all copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 | THE SOFTWARE.
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # bpmn-js-embedded-comments
2 |
3 | An extension for [bpmn-js](https://github.com/bpmn-io/bpmn-js) that allows you to comment on a BPMN 2.0 diagram.
4 |
5 | Stores the comments within the BPMN 2.0 XML file.
6 |
7 | 
8 |
9 |
10 | ## How comments are stored
11 |
12 | Comments are read, added and written to an elements `` tag.
13 |
14 | The format for comments is assumed to be
15 |
16 | ```
17 | author:comment;
18 | ;other-author:other comment
19 | canbe multiline, too
20 | ```
21 |
22 | ## License
23 |
24 | MIT
25 |
--------------------------------------------------------------------------------
/assets/comments.css:
--------------------------------------------------------------------------------
1 | .comments-overlay {
2 | background: #CCC;
3 | padding: 5px;
4 | border-radius: 5px;
5 | }
6 |
7 | .comments-overlay.expanded {
8 | z-index: 100;
9 | position: absolute; /* for z-index to work */
10 | }
11 |
12 | .comments-overlay .toggle {
13 | white-space: nowrap;
14 | }
15 |
16 | .comments-overlay.expanded .comment-count,
17 | .comments-overlay .content {
18 | display: none;
19 | }
20 |
21 | .comments-overlay .comment-count {
22 | display: inline;
23 | }
24 |
25 | .comments-overlay.expanded .content {
26 | display: block;
27 | }
28 |
29 | .comments-overlay .comment {
30 | border-top: dotted 1px #666;
31 | margin-top: 4px;
32 | padding-top: 4px;
33 | white-space: pre-wrap;
34 |
35 | position: relative;
36 | }
37 |
38 | .comments-overlay .comment .delete {
39 | position: absolute;
40 | right: 3px;
41 | top: 3px;
42 |
43 | text-decoration: none;
44 |
45 | display: none;
46 | }
47 |
48 | .comments-overlay .comment:hover .delete {
49 | display: block;
50 | }
51 |
52 | .comments-overlay .edit {
53 |
54 | }
--------------------------------------------------------------------------------
/docs/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bpmn-io/bpmn-js-embedded-comments/2943663d3c8459dbf48c4a22aab39c27303b3c41/docs/screenshot.png
--------------------------------------------------------------------------------
/karma.conf.js:
--------------------------------------------------------------------------------
1 | /* eslint-env node */
2 |
3 | // configures browsers to run test against
4 | // any of [ 'ChromeHeadless', 'Chrome', 'Firefox' ]
5 | const browsers =
6 | (process.env.TEST_BROWSERS || 'ChromeHeadless')
7 | .replace(/^\s+|\s+$/, '')
8 | .split(/\s*,\s*/g);
9 |
10 | const suite = 'test/suite.js';
11 |
12 |
13 | module.exports = function(karma) {
14 | karma.set({
15 |
16 | frameworks: [
17 | 'mocha',
18 | 'sinon-chai',
19 | 'webpack'
20 | ],
21 |
22 | files: [
23 | suite
24 | ],
25 |
26 | preprocessors: {
27 | [suite]: [ 'webpack' ]
28 | },
29 |
30 | reporters: [ 'progress' ],
31 |
32 | browsers,
33 |
34 | autoWatch: false,
35 | singleRun: true,
36 |
37 | webpack: {
38 | mode: 'development',
39 | module: {
40 | rules: [
41 | {
42 | test: /\.(css|bpmn)$/,
43 | type: 'asset/source'
44 | }
45 | ]
46 | }
47 | }
48 | });
49 | };
50 |
--------------------------------------------------------------------------------
/lib/comments.js:
--------------------------------------------------------------------------------
1 | import $ from 'jquery';
2 |
3 | import {
4 | getComments,
5 | removeComment,
6 | addComment
7 | } from './util';
8 |
9 | var COMMENT_HTML =
10 | '
';
13 |
14 | export default function Comments(eventBus, overlays, bpmnjs, translate) {
15 | function toggleCollapse(element) {
16 |
17 | var o = overlays.get({ element: element, type: 'comments' })[0];
18 |
19 | var $overlay = o && o.html;
20 |
21 | if ($overlay) {
22 |
23 | var expanded = $overlay.is('.expanded');
24 |
25 | eventBus.fire('comments.toggle', { element: element, active: !expanded });
26 |
27 | if (expanded) {
28 | $overlay.removeClass('expanded');
29 | } else {
30 | $overlay.addClass('expanded');
31 | $overlay.find('textarea').focus();
32 | }
33 | }
34 | }
35 |
36 | function createCommentBox(element) {
37 |
38 | var $overlay = $(getOverlayHtml(translate));
39 |
40 | $overlay.find('.toggle').click(function(e) {
41 | toggleCollapse(element);
42 | });
43 |
44 | var $commentCount = $overlay.find('[data-comment-count]'),
45 | $textarea = $overlay.find('textarea'),
46 | $comments = $overlay.find('.comments');
47 |
48 |
49 | function renderComments() {
50 |
51 | // clear innerHTML
52 | $comments.html('');
53 |
54 | var comments = getComments(element);
55 |
56 | comments.forEach(function(val) {
57 | var $comment = $(COMMENT_HTML);
58 |
59 | $comment.find('[data-text]').text(val[1]);
60 | $comment.find('[data-delete]').click(function(e) {
61 |
62 | e.preventDefault();
63 |
64 | removeComment(element, val);
65 | renderComments();
66 | $textarea.val(val[1]);
67 | });
68 |
69 | $comments.append($comment);
70 | });
71 |
72 | $overlay[comments.length ? 'addClass' : 'removeClass']('with-comments');
73 |
74 | $commentCount.text(comments.length ? ('(' + comments.length + ')') : '');
75 |
76 | eventBus.fire('comments.updated', { comments: comments });
77 | }
78 |
79 | $textarea.on('keydown', function(e) {
80 | if (e.which === 13 && !e.shiftKey) {
81 | e.preventDefault();
82 |
83 | var comment = $textarea.val();
84 |
85 | if (comment) {
86 | addComment(element, '', comment);
87 | $textarea.val('');
88 | renderComments();
89 | }
90 | }
91 | });
92 |
93 |
94 | // attach an overlay to a node
95 | overlays.add(element, 'comments', {
96 | position: {
97 | bottom: 10,
98 | right: 10
99 | },
100 | html: $overlay
101 | });
102 |
103 | renderComments();
104 | }
105 |
106 | eventBus.on('shape.added', function(event) {
107 | var element = event.element;
108 |
109 | if (element.labelTarget ||
110 | !element.businessObject.$instanceOf('bpmn:FlowNode')) {
111 |
112 | return;
113 | }
114 |
115 | defer(function() {
116 | createCommentBox(element);
117 | });
118 |
119 | });
120 |
121 | this.collapseAll = function() {
122 |
123 | overlays.get({ type: 'comments' }).forEach(function(c) {
124 | var html = c.html;
125 | if (html.is('.expanded')) {
126 | toggleCollapse(c.element);
127 | }
128 | });
129 |
130 | };
131 | }
132 |
133 | Comments.$inject = [ 'eventBus', 'overlays', 'bpmnjs', 'translate' ];
134 |
135 |
136 | // helpers ///////////////
137 |
138 | function defer(fn) {
139 | setTimeout(fn, 0);
140 | }
141 |
142 | function getOverlayHtml(translate) {
143 | return '';
155 | }
--------------------------------------------------------------------------------
/lib/index.js:
--------------------------------------------------------------------------------
1 | import Comments from './comments';
2 |
3 | export default {
4 | __init__: [ 'comments' ],
5 | 'comments': [ 'type', Comments ]
6 | };
--------------------------------------------------------------------------------
/lib/util.js:
--------------------------------------------------------------------------------
1 | export function _getCommentsElement(element, create) {
2 |
3 | var bo = element.businessObject;
4 |
5 | var docs = bo.get('documentation');
6 |
7 | var comments;
8 |
9 | // get comments node
10 | docs.some(function(d) {
11 | return d.textFormat === 'text/x-comments' && (comments = d);
12 | });
13 |
14 | // create if not existing
15 | if (!comments && create) {
16 | comments = bo.$model.create('bpmn:Documentation', { textFormat: 'text/x-comments' });
17 | docs.push(comments);
18 | }
19 |
20 | return comments;
21 | }
22 |
23 | export function getComments(element) {
24 | var doc = _getCommentsElement(element);
25 |
26 | if (!doc || !doc.text) {
27 | return [];
28 | } else {
29 | return doc.text.split(/;\r?\n;/).map(function(str) {
30 | return str.split(/:/, 2);
31 | });
32 | }
33 | }
34 |
35 | export function setComments(element, comments) {
36 | var doc = _getCommentsElement(element, true);
37 |
38 | var str = comments.map(function(c) {
39 | return c.join(':');
40 | }).join(';\n;');
41 |
42 | doc.text = str;
43 | }
44 |
45 | export function addComment(element, author, str) {
46 | var comments = getComments(element);
47 |
48 | comments.push([ author, str ]);
49 |
50 | setComments(element, comments);
51 | }
52 |
53 | export function removeComment(element, comment) {
54 | var comments = getComments(element);
55 |
56 | var idx = -1;
57 |
58 | comments.some(function(c, i) {
59 |
60 | var matches = c[0] === comment[0] && c[1] === comment[1];
61 |
62 | if (matches) {
63 | idx = i;
64 | }
65 |
66 | return matches;
67 | });
68 |
69 | if (idx !== -1) {
70 | comments.splice(idx, 1);
71 | }
72 |
73 | setComments(element, comments);
74 | }
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "bpmn-js-embedded-comments",
3 | "version": "0.7.0",
4 | "description": "A simple comments extension for bpmn-js",
5 | "scripts": {
6 | "all": "run-s lint test bundle",
7 | "lint": "eslint .",
8 | "dev": "npm test -- --auto-watch --no-single-run",
9 | "test": "karma start",
10 | "bundle": "microbundle build --no-compress --name BpmnJSEmbeddedComments",
11 | "prepublishOnly": "run-s bundle"
12 | },
13 | "main": "dist/index.js",
14 | "module": "dist/index.esm.js",
15 | "umd:main": "dist/bpmn-js-embedded-comments.umd.js",
16 | "source": "lib/index.js",
17 | "repository": {
18 | "type": "git",
19 | "url": "https://github.com/bpmn-io/bpmn-js-embedded-comments"
20 | },
21 | "keywords": [
22 | "bpmn-js",
23 | "bpmn-js-plugin"
24 | ],
25 | "author": "bpmn.io",
26 | "license": "MIT",
27 | "devDependencies": {
28 | "bpmn-js": "^10.2.1",
29 | "chai": "^4.3.6",
30 | "eslint": "^8.27.0",
31 | "eslint-plugin-bpmn-io": "^0.16.0",
32 | "karma": "^6.4.1",
33 | "karma-chrome-launcher": "^3.1.1",
34 | "karma-mocha": "^2.0.1",
35 | "karma-sinon-chai": "^2.0.2",
36 | "karma-webpack": "^5.0.0",
37 | "microbundle": "^0.15.1",
38 | "mocha": "^10.1.0",
39 | "mocha-test-container-support": "^0.2.0",
40 | "npm-run-all": "^4.1.5",
41 | "puppeteer": "^19.2.2",
42 | "sinon": "^9.2.4",
43 | "sinon-chai": "^3.7.0",
44 | "webpack": "^5.74.0"
45 | },
46 | "dependencies": {
47 | "jquery": "^3.6.1"
48 | },
49 | "files": [
50 | "dist",
51 | "assets"
52 | ]
53 | }
54 |
--------------------------------------------------------------------------------
/test/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "plugin:bpmn-io/mocha"
3 | }
--------------------------------------------------------------------------------
/test/assets/comments-test.css:
--------------------------------------------------------------------------------
1 | .icon-comment {
2 | background: fuchsia;
3 | display: inline-block;
4 | width: 10px;
5 | height: 10px;
6 | }
7 |
8 | .icon-delete::before {
9 | content: '×'
10 | }
--------------------------------------------------------------------------------
/test/spec/commentsSpec.js:
--------------------------------------------------------------------------------
1 | import '../test-helper';
2 |
3 | import Viewer from 'bpmn-js/lib/Viewer';
4 |
5 | import commentsModule from '../../lib';
6 |
7 | import {
8 | addComment
9 | } from '../../lib/util';
10 |
11 |
12 | describe('comments integration', function() {
13 |
14 | let container;
15 |
16 | beforeEach(function() {
17 | container = document.createElement('div');
18 | container.classList.add('test-container');
19 | document.body.appendChild(container);
20 | });
21 |
22 |
23 | this.timeout(5000);
24 |
25 |
26 | it('should open viewer with comments extension', async function() {
27 |
28 | // given
29 | const viewer = new Viewer({
30 | container: container,
31 | additionalModules: [
32 | commentsModule
33 | ]
34 | });
35 |
36 | const xml = require('./simple-with-comments.bpmn');
37 |
38 |
39 | // when
40 | await viewer.importXML(xml);
41 |
42 | await defer();
43 |
44 | const overlays = viewer.get('overlays');
45 |
46 | const overlay = overlays.get({ element: 'Task_1', type: 'comments' })[0];
47 |
48 | const text = overlay.html.text();
49 |
50 | expect(text).to.contain('(2)');
51 | expect(text).to.contain('LINEBREAK');
52 | expect(text).to.contain('TEST');
53 | });
54 |
55 |
56 | it('should serialize with new comment', async function() {
57 |
58 | // given
59 | const viewer = new Viewer({
60 | container: container,
61 | additionalModules: [
62 | commentsModule
63 | ]
64 | });
65 |
66 | const xml = require('./simple.bpmn');
67 |
68 |
69 | // when
70 | await viewer.importXML(xml);
71 |
72 | await defer();
73 |
74 | const elementRegistry = viewer.get('elementRegistry');
75 |
76 | const subProcess = elementRegistry.get('SubProcess_1');
77 |
78 | addComment(subProcess, '', 'This is a subprocess');
79 | addComment(subProcess, 'ME', 'This is another comment\n(with line breaks)');
80 |
81 | const expectedXML =
82 | '' +
83 | '' +
84 | ':This is a subprocess;\n' +
85 | ';ME:This is another comment\n' +
86 | '(with line breaks)' +
87 | ''; // ...
88 |
89 | const { xml: savedXML } = await viewer.saveXML();
90 |
91 | expect(savedXML).to.contain(expectedXML);
92 |
93 | });
94 |
95 | });
96 |
97 |
98 | // helpers ///////////////
99 |
100 | function defer(fn) {
101 | return new Promise(resolve => {
102 | setTimeout(resolve, 0);
103 | });
104 | }
--------------------------------------------------------------------------------
/test/spec/simple-with-comments.bpmn:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | SequenceFlow_2
6 |
7 | SequenceFlow_1
8 |
9 |
10 | :TEST;
11 | ;:WITH
12 | LINEBREAKs
13 |
14 | SequenceFlow_1
15 |
16 |
17 |
18 |
19 | SequenceFlow_2
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 |
--------------------------------------------------------------------------------
/test/spec/simple.bpmn:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | SequenceFlow_2
6 |
7 | SequenceFlow_1
8 |
9 |
10 | SequenceFlow_1
11 |
12 |
13 |
14 |
15 | SequenceFlow_2
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 |
--------------------------------------------------------------------------------
/test/suite.js:
--------------------------------------------------------------------------------
1 | var allTests = require.context('.', true, /Spec\.js$/);
2 |
3 | allTests.keys().forEach(allTests);
--------------------------------------------------------------------------------
/test/test-helper.js:
--------------------------------------------------------------------------------
1 | import {
2 | insertCSS
3 | } from 'bpmn-js/test/helper';
4 |
5 | insertCSS('diagram-js.css', require('bpmn-js/dist/assets/diagram-js.css'));
6 | insertCSS('comments.css', require('../assets/comments.css'));
7 |
8 | insertCSS('comments-test.css', require('./assets/comments-test.css'));
9 |
10 | insertCSS('test.css', '.test-container { height: 80vh; width: 80vw; border: solid #CACACA 3px; margin: 10px 0; }');
11 |
12 | export * from 'bpmn-js/test/helper';
--------------------------------------------------------------------------------