├── .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 | ![A screenshot of a comments integration](https://raw.githubusercontent.com/bpmn-io/bpmn-js-embedded-comments/master/docs/screenshot.png) 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 | '
' + 11 | '
' + 12 | '
'; 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 '
' + 144 | '
' + 145 | '' + 146 | '' + 147 | '
' + 148 | '
' + 149 | '
' + 150 | '
' + 151 | `` + 152 | '
' + 153 | '
' + 154 | '
'; 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'; --------------------------------------------------------------------------------