├── .github
└── workflows
│ └── CI.yml
├── .gitignore
├── CHANGELOG.md
├── LICENSE
├── README.md
├── eslint.config.mjs
├── karma.conf.js
├── lib
├── DirectEditing.js
├── TextBox.js
└── index.js
├── package-lock.json
├── package.json
├── renovate.json
├── resources
└── screencast.gif
└── test
├── DirectEditingProvider.js
├── DirectEditingSpec.js
└── suite.js
/.github/workflows/CI.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 | on: [ push, pull_request ]
3 | jobs:
4 | Build:
5 | runs-on: ubuntu-latest
6 |
7 | strategy:
8 | matrix:
9 | integration-deps:
10 | - ""
11 | - "diagram-js@^14"
12 |
13 | steps:
14 | - name: Checkout
15 | uses: actions/checkout@v4
16 | - name: Use Node.js 20
17 | uses: actions/setup-node@v4
18 | with:
19 | node-version: 20
20 | cache: 'npm'
21 | - name: Install dependencies
22 | run: npm ci
23 | - name: Install dependencies for integration test
24 | if: ${{ matrix.integration-deps != '' }}
25 | run: npm install ${{ matrix.integration-deps }}
26 | - name: Setup project
27 | uses: bpmn-io/actions/setup@latest
28 | - name: Build
29 | env:
30 | TEST_BROWSERS: ChromeHeadless
31 | run: npm run all
32 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | .idea/
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | All notable changes to [diagram-js-direct-editing](https://github.com/bpmn-io/diagram-js-direct-editing) are documented here. We use [semantic versioning](http://semver.org/) for releases.
4 |
5 | ## Unreleased
6 |
7 | ___Note:__ Yet to be released changes appear here._
8 |
9 | ## 3.2.0
10 |
11 | * `FEAT`: restore focus on Canvas after close
12 | * `FEAT`: support `diagram-js@15.0.0`
13 |
14 | ## 3.1.0
15 |
16 | * `DEPS`: update to `min-dom@4.2.1`
17 |
18 | ## 3.0.1
19 |
20 | _This reverts `v3.0.0`._
21 |
22 | * `FEAT`: restore background for all textboxes. You can remove the background with custom styles or a style config in direct editing provider.
23 |
24 | ## 3.0.0
25 |
26 | * `FEAT`: remove background for non-resizable textboxes ([#23](https://github.com/bpmn-io/diagram-js-direct-editing/issues/23))
27 |
28 | ### Breaking Changes
29 |
30 | * By default, no background is shown when editing a static sized element. To restore old behavior, add a style config when activating direct editing:
31 | ```
32 | const MyProvider = {
33 | activate: (element) => {
34 | return {
35 | style: {
36 | backgroundColor: '#ffffff',
37 | border: '1px solid #ccc'
38 | }
39 | // ...
40 | }
41 | }
42 | }
43 | ```
44 |
45 | ## 2.1.2
46 |
47 | _This reverts `v2.1.1`._
48 |
49 | * `FIX`: restore `main` package export
50 |
51 | ## 2.1.1
52 |
53 | * `FIX`: drop `main` package export
54 |
55 | ## 2.1.0
56 |
57 | * `FEAT`: allow loading as a module
58 |
59 | ## 2.0.1
60 |
61 | * `FIX`: add package export
62 |
63 | ## 2.0.0
64 |
65 | * `DEPS`: bump utility dependencies
66 |
67 | ## 1.8.0
68 |
69 | * `DEPS`: support diagram-js@9
70 |
71 | ## 1.7.0
72 |
73 | * `FEAT`: allow to query for active element ([#25](https://github.com/bpmn-io/diagram-js-direct-editing/pull/25))
74 |
75 | ## 1.6.4
76 |
77 | * `DEPS`: support diagram-js@8
78 |
79 | ## 1.6.3
80 |
81 | * `FIX`: preserve Windows newline characters ([#19](https://github.com/bpmn-io/diagram-js-direct-editing/pull/19))
82 |
83 | ## 1.6.2
84 |
85 | * `DEPS`: support diagram-js@7
86 |
87 | ## 1.6.1
88 |
89 | * `DEPS`: support diagram-js@6
90 |
91 | ## 1.6.0
92 |
93 | * `DEPS`: support diagram-js@5
94 |
95 | ## 1.5.0
96 |
97 | * `DEPS`: support diagram-js@4
98 |
99 | ## 1.4.3
100 |
101 | * `FIX`: prevent injection of HTML and JS evaluation on paste ([#13](https://github.com/bpmn-io/diagram-js-direct-editing/issues/13))
102 |
103 | ## 1.4.2
104 |
105 | * `FIX`: only trigger update if text or bounds changed ([#11](https://github.com/bpmn-io/diagram-js-direct-editing/pull/11))
106 |
107 | ## 1.4.1
108 |
109 | * `FIX`: ignore superfluous whitespace around labels
110 | * `FIX`: return correct updated text box bounds
111 | * `CHORE`: only compute text box bounds if actually necessary
112 |
113 | ## 1.4.0
114 |
115 | * `FEAT`: mark as compatible to `diagram-js@3`
116 | * `FEAT`: accept `fontFamily` and `fontWeight` styles
117 | * `CHORE`: use `box-sizing: border-box` for proper computation of
118 |
119 | ## 1.2.2
120 |
121 | _This reverts v1.2.1._
122 |
123 | ## 1.2.1
124 |
125 | * `FIX`: use `textContent` to retrieve correct editing values in IE 11 ([#5](https://github.com/bpmn-io/diagram-js-direct-editing/issues/5))
126 |
127 | ## ...
128 |
129 | Check `git log` for earlier history.
130 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2014-2017 camunda Services GmbH
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # diagram-js-direct-editing
2 |
3 | [](https://github.com/bpmn-io/diagram-js-direct-editing/actions?query=workflow%3ACI)
4 |
5 | A direct editing box for [diagram-js](https://github.com/bpmn-io/diagram-js).
6 |
7 | 
8 |
9 |
10 | ## License
11 |
12 | MIT
--------------------------------------------------------------------------------
/eslint.config.mjs:
--------------------------------------------------------------------------------
1 | import bpmnIoPlugin from 'eslint-plugin-bpmn-io';
2 |
3 | const files = {
4 | build: [
5 | '*.js',
6 | '*.mjs'
7 | ],
8 | test: [
9 | 'test/**/*.js'
10 | ]
11 | };
12 |
13 | export default [
14 |
15 | // build
16 | ...bpmnIoPlugin.configs.node.map((config) => {
17 | return {
18 | ...config,
19 | files: files.build
20 | };
21 | }),
22 |
23 | // lib + test
24 | ...bpmnIoPlugin.configs.browser.map((config) => {
25 | return {
26 | ...config,
27 | ignores: files.build
28 | };
29 | }),
30 |
31 | // test
32 | ...bpmnIoPlugin.configs.mocha.map((config) => {
33 | return {
34 | ...config,
35 | files: files.test
36 | };
37 | })
38 | ];
--------------------------------------------------------------------------------
/karma.conf.js:
--------------------------------------------------------------------------------
1 | /* eslint-env node */
2 |
3 | // configures browsers to run test against
4 | // any of [ 'ChromeHeadless', 'Chrome', 'Firefox' ]
5 | const browsers = (process.env.TEST_BROWSERS || 'ChromeHeadless').split(',');
6 |
7 | // use puppeteer provided Chrome for testing
8 | process.env.CHROME_BIN = require('puppeteer').executablePath();
9 |
10 |
11 | module.exports = function(karma) {
12 |
13 | karma.set({
14 | frameworks: [
15 | 'webpack',
16 | 'mocha',
17 | 'sinon-chai'
18 | ],
19 |
20 | files: [
21 | 'test/suite.js'
22 | ],
23 |
24 | preprocessors: {
25 | 'test/suite.js': [ 'webpack' ]
26 | },
27 |
28 | reporters: [ 'progress' ],
29 |
30 | browsers,
31 |
32 | singleRun: true,
33 | autoWatch: false,
34 |
35 | webpack: {
36 | mode: 'development'
37 | }
38 | });
39 |
40 | };
41 |
--------------------------------------------------------------------------------
/lib/DirectEditing.js:
--------------------------------------------------------------------------------
1 | import {
2 | bind,
3 | find
4 | } from 'min-dash';
5 |
6 | import TextBox from './TextBox.js';
7 |
8 |
9 | /**
10 | * A direct editing component that allows users
11 | * to edit an elements text directly in the diagram
12 | *
13 | * @param {EventBus} eventBus the event bus
14 | * @param {Canvas} canvas the canvas
15 | */
16 | export default function DirectEditing(eventBus, canvas) {
17 |
18 | this._eventBus = eventBus;
19 | this._canvas = canvas;
20 |
21 | this._providers = [];
22 | this._textbox = new TextBox({
23 | container: canvas.getContainer(),
24 | keyHandler: bind(this._handleKey, this),
25 | resizeHandler: bind(this._handleResize, this)
26 | });
27 | }
28 |
29 | DirectEditing.$inject = [ 'eventBus', 'canvas' ];
30 |
31 |
32 | /**
33 | * Register a direct editing provider
34 |
35 | * @param {Object} provider the provider, must expose an #activate(element) method that returns
36 | * an activation context ({ bounds: {x, y, width, height }, text }) if
37 | * direct editing is available for the given element.
38 | * Additionally the provider must expose a #update(element, value) method
39 | * to receive direct editing updates.
40 | */
41 | DirectEditing.prototype.registerProvider = function(provider) {
42 | this._providers.push(provider);
43 | };
44 |
45 |
46 | /**
47 | * Returns true if direct editing is currently active
48 | *
49 | * @param {djs.model.Base} [element]
50 | *
51 | * @return {boolean}
52 | */
53 | DirectEditing.prototype.isActive = function(element) {
54 | return !!(this._active && (!element || this._active.element === element));
55 | };
56 |
57 |
58 | /**
59 | * Cancel direct editing, if it is currently active
60 | */
61 | DirectEditing.prototype.cancel = function() {
62 | if (!this._active) {
63 | return;
64 | }
65 |
66 | this._fire('cancel');
67 | this.close();
68 | };
69 |
70 |
71 | DirectEditing.prototype._fire = function(event, context) {
72 | this._eventBus.fire('directEditing.' + event, context || { active: this._active });
73 | };
74 |
75 | DirectEditing.prototype.close = function() {
76 | this._textbox.destroy();
77 |
78 | this._fire('deactivate');
79 |
80 | this._active = null;
81 |
82 | this.resizable = undefined;
83 |
84 | // restoreFocus API is available from diagram-js@15.0.0
85 | this._canvas.restoreFocus && this._canvas.restoreFocus();
86 | };
87 |
88 |
89 | DirectEditing.prototype.complete = function() {
90 |
91 | var active = this._active;
92 |
93 | if (!active) {
94 | return;
95 | }
96 |
97 | var containerBounds,
98 | previousBounds = active.context.bounds,
99 | newBounds = this.$textbox.getBoundingClientRect(),
100 | newText = this.getValue(),
101 | previousText = active.context.text;
102 |
103 | if (
104 | newText !== previousText ||
105 | newBounds.height !== previousBounds.height ||
106 | newBounds.width !== previousBounds.width
107 | ) {
108 | containerBounds = this._textbox.container.getBoundingClientRect();
109 |
110 | active.provider.update(active.element, newText, active.context.text, {
111 | x: newBounds.left - containerBounds.left,
112 | y: newBounds.top - containerBounds.top,
113 | width: newBounds.width,
114 | height: newBounds.height
115 | });
116 | }
117 |
118 | this._fire('complete');
119 |
120 | this.close();
121 | };
122 |
123 |
124 | DirectEditing.prototype.getValue = function() {
125 | return this._textbox.getValue();
126 | };
127 |
128 |
129 | DirectEditing.prototype._handleKey = function(e) {
130 |
131 | // stop bubble
132 | e.stopPropagation();
133 |
134 | var key = e.keyCode || e.charCode;
135 |
136 | // ESC
137 | if (key === 27) {
138 | e.preventDefault();
139 | return this.cancel();
140 | }
141 |
142 | // Enter
143 | if (key === 13 && !e.shiftKey) {
144 | e.preventDefault();
145 | return this.complete();
146 | }
147 | };
148 |
149 |
150 | DirectEditing.prototype._handleResize = function(event) {
151 | this._fire('resize', event);
152 | };
153 |
154 |
155 | /**
156 | * Activate direct editing on the given element
157 | *
158 | * @param {Object} ElementDescriptor the descriptor for a shape or connection
159 | * @return {Boolean} true if the activation was possible
160 | */
161 | DirectEditing.prototype.activate = function(element) {
162 | if (this.isActive()) {
163 | this.cancel();
164 | }
165 |
166 | // the direct editing context
167 | var context;
168 |
169 | var provider = find(this._providers, function(p) {
170 | return ((context = p.activate(element))) ? p : null;
171 | });
172 |
173 | // check if activation took place
174 | if (context) {
175 | this.$textbox = this._textbox.create(
176 | context.bounds,
177 | context.style,
178 | context.text,
179 | context.options
180 | );
181 |
182 | this._active = {
183 | element: element,
184 | context: context,
185 | provider: provider
186 | };
187 |
188 | if (context.options && context.options.resizable) {
189 | this.resizable = true;
190 | }
191 |
192 | this._fire('activate');
193 | }
194 |
195 | return !!context;
196 | };
197 |
--------------------------------------------------------------------------------
/lib/TextBox.js:
--------------------------------------------------------------------------------
1 | import {
2 | assign,
3 | bind,
4 | pick
5 | } from 'min-dash';
6 |
7 | import {
8 | domify,
9 | query as domQuery,
10 | event as domEvent,
11 | remove as domRemove
12 | } from 'min-dom';
13 |
14 | var min = Math.min,
15 | max = Math.max;
16 |
17 | function preventDefault(e) {
18 | e.preventDefault();
19 | }
20 |
21 | function stopPropagation(e) {
22 | e.stopPropagation();
23 | }
24 |
25 | function isTextNode(node) {
26 | return node.nodeType === Node.TEXT_NODE;
27 | }
28 |
29 | function toArray(nodeList) {
30 | return [].slice.call(nodeList);
31 | }
32 |
33 | /**
34 | * Initializes a container for a content editable div.
35 | *
36 | * Structure:
37 | *
38 | * container
39 | * parent
40 | * content
41 | * resize-handle
42 | *
43 | * @param {object} options
44 | * @param {DOMElement} options.container The DOM element to append the contentContainer to
45 | * @param {Function} options.keyHandler Handler for key events
46 | * @param {Function} options.resizeHandler Handler for resize events
47 | */
48 | export default function TextBox(options) {
49 | this.container = options.container;
50 |
51 | this.parent = domify(
52 | '
'
55 | );
56 |
57 | this.content = domQuery('[contenteditable]', this.parent);
58 |
59 | this.keyHandler = options.keyHandler || function() {};
60 | this.resizeHandler = options.resizeHandler || function() {};
61 |
62 | this.autoResize = bind(this.autoResize, this);
63 | this.handlePaste = bind(this.handlePaste, this);
64 | }
65 |
66 |
67 | /**
68 | * Create a text box with the given position, size, style and text content
69 | *
70 | * @param {Object} bounds
71 | * @param {Number} bounds.x absolute x position
72 | * @param {Number} bounds.y absolute y position
73 | * @param {Number} [bounds.width] fixed width value
74 | * @param {Number} [bounds.height] fixed height value
75 | * @param {Number} [bounds.maxWidth] maximum width value
76 | * @param {Number} [bounds.maxHeight] maximum height value
77 | * @param {Number} [bounds.minWidth] minimum width value
78 | * @param {Number} [bounds.minHeight] minimum height value
79 | * @param {Object} [style]
80 | * @param {String} value text content
81 | *
82 | * @return {DOMElement} The created content DOM element
83 | */
84 | TextBox.prototype.create = function(bounds, style, value, options) {
85 | var self = this;
86 |
87 | var parent = this.parent,
88 | content = this.content,
89 | container = this.container;
90 |
91 | options = this.options = options || {};
92 |
93 | style = this.style = style || {};
94 |
95 | var parentStyle = pick(style, [
96 | 'width',
97 | 'height',
98 | 'maxWidth',
99 | 'maxHeight',
100 | 'minWidth',
101 | 'minHeight',
102 | 'left',
103 | 'top',
104 | 'backgroundColor',
105 | 'position',
106 | 'overflow',
107 | 'border',
108 | 'wordWrap',
109 | 'textAlign',
110 | 'outline',
111 | 'transform'
112 | ]);
113 |
114 | assign(parent.style, {
115 | width: bounds.width + 'px',
116 | height: bounds.height + 'px',
117 | maxWidth: bounds.maxWidth + 'px',
118 | maxHeight: bounds.maxHeight + 'px',
119 | minWidth: bounds.minWidth + 'px',
120 | minHeight: bounds.minHeight + 'px',
121 | left: bounds.x + 'px',
122 | top: bounds.y + 'px',
123 | backgroundColor: '#ffffff',
124 | position: 'absolute',
125 | overflow: 'visible',
126 | border: '1px solid #ccc',
127 | boxSizing: 'border-box',
128 | wordWrap: 'normal',
129 | textAlign: 'center',
130 | outline: 'none'
131 | }, parentStyle);
132 |
133 | var contentStyle = pick(style, [
134 | 'fontFamily',
135 | 'fontSize',
136 | 'fontWeight',
137 | 'lineHeight',
138 | 'padding',
139 | 'paddingTop',
140 | 'paddingRight',
141 | 'paddingBottom',
142 | 'paddingLeft'
143 | ]);
144 |
145 | assign(content.style, {
146 | boxSizing: 'border-box',
147 | width: '100%',
148 | outline: 'none',
149 | wordWrap: 'break-word'
150 | }, contentStyle);
151 |
152 | if (options.centerVertically) {
153 | assign(content.style, {
154 | position: 'absolute',
155 | top: '50%',
156 | transform: 'translate(0, -50%)'
157 | }, contentStyle);
158 | }
159 |
160 | content.innerText = value;
161 |
162 | domEvent.bind(content, 'keydown', this.keyHandler);
163 | domEvent.bind(content, 'mousedown', stopPropagation);
164 | domEvent.bind(content, 'paste', self.handlePaste);
165 |
166 | if (options.autoResize) {
167 | domEvent.bind(content, 'input', this.autoResize);
168 | }
169 |
170 | if (options.resizable) {
171 | this.resizable(style);
172 | }
173 |
174 | container.appendChild(parent);
175 |
176 | // set selection to end of text
177 | this.setSelection(content.lastChild, content.lastChild && content.lastChild.length);
178 |
179 | return parent;
180 | };
181 |
182 | /**
183 | * Intercept paste events to remove formatting from pasted text.
184 | */
185 | TextBox.prototype.handlePaste = function(e) {
186 | var options = this.options,
187 | style = this.style;
188 |
189 | e.preventDefault();
190 |
191 | var text;
192 |
193 | if (e.clipboardData) {
194 |
195 | // Chrome, Firefox, Safari
196 | text = e.clipboardData.getData('text/plain');
197 | } else {
198 |
199 | // Internet Explorer
200 | text = window.clipboardData.getData('Text');
201 | }
202 |
203 | this.insertText(text);
204 |
205 | if (options.autoResize) {
206 | var hasResized = this.autoResize(style);
207 |
208 | if (hasResized) {
209 | this.resizeHandler(hasResized);
210 | }
211 | }
212 | };
213 |
214 | TextBox.prototype.insertText = function(text) {
215 | text = normalizeEndOfLineSequences(text);
216 |
217 | // insertText command not supported by Internet Explorer
218 | var success = document.execCommand('insertText', false, text);
219 |
220 | if (success) {
221 | return;
222 | }
223 |
224 | this._insertTextIE(text);
225 | };
226 |
227 | TextBox.prototype._insertTextIE = function(text) {
228 |
229 | // Internet Explorer
230 | var range = this.getSelection(),
231 | startContainer = range.startContainer,
232 | endContainer = range.endContainer,
233 | startOffset = range.startOffset,
234 | endOffset = range.endOffset,
235 | commonAncestorContainer = range.commonAncestorContainer;
236 |
237 | var childNodesArray = toArray(commonAncestorContainer.childNodes);
238 |
239 | var container,
240 | offset;
241 |
242 | if (isTextNode(commonAncestorContainer)) {
243 | var containerTextContent = startContainer.textContent;
244 |
245 | startContainer.textContent =
246 | containerTextContent.substring(0, startOffset)
247 | + text
248 | + containerTextContent.substring(endOffset);
249 |
250 | container = startContainer;
251 | offset = startOffset + text.length;
252 |
253 | } else if (startContainer === this.content && endContainer === this.content) {
254 | var textNode = document.createTextNode(text);
255 |
256 | this.content.insertBefore(textNode, childNodesArray[startOffset]);
257 |
258 | container = textNode;
259 | offset = textNode.textContent.length;
260 | } else {
261 | var startContainerChildIndex = childNodesArray.indexOf(startContainer),
262 | endContainerChildIndex = childNodesArray.indexOf(endContainer);
263 |
264 | childNodesArray.forEach(function(childNode, index) {
265 |
266 | if (index === startContainerChildIndex) {
267 | childNode.textContent =
268 | startContainer.textContent.substring(0, startOffset) +
269 | text +
270 | endContainer.textContent.substring(endOffset);
271 | } else if (index > startContainerChildIndex && index <= endContainerChildIndex) {
272 | domRemove(childNode);
273 | }
274 | });
275 |
276 | container = startContainer;
277 | offset = startOffset + text.length;
278 | }
279 |
280 | if (container && offset !== undefined) {
281 |
282 | // is necessary in Internet Explorer
283 | setTimeout(function() {
284 | self.setSelection(container, offset);
285 | });
286 | }
287 | };
288 |
289 | /**
290 | * Automatically resize element vertically to fit its content.
291 | */
292 | TextBox.prototype.autoResize = function() {
293 | var parent = this.parent,
294 | content = this.content;
295 |
296 | var fontSize = parseInt(this.style.fontSize) || 12;
297 |
298 | if (content.scrollHeight > parent.offsetHeight ||
299 | content.scrollHeight < parent.offsetHeight - fontSize) {
300 | var bounds = parent.getBoundingClientRect();
301 |
302 | var height = content.scrollHeight;
303 | parent.style.height = height + 'px';
304 |
305 | this.resizeHandler({
306 | width: bounds.width,
307 | height: bounds.height,
308 | dx: 0,
309 | dy: height - bounds.height
310 | });
311 | }
312 | };
313 |
314 | /**
315 | * Make an element resizable by adding a resize handle.
316 | */
317 | TextBox.prototype.resizable = function() {
318 | var self = this;
319 |
320 | var parent = this.parent,
321 | resizeHandle = this.resizeHandle;
322 |
323 | var minWidth = parseInt(this.style.minWidth) || 0,
324 | minHeight = parseInt(this.style.minHeight) || 0,
325 | maxWidth = parseInt(this.style.maxWidth) || Infinity,
326 | maxHeight = parseInt(this.style.maxHeight) || Infinity;
327 |
328 | if (!resizeHandle) {
329 | resizeHandle = this.resizeHandle = domify(
330 | ''
331 | );
332 |
333 | var startX, startY, startWidth, startHeight;
334 |
335 | var onMouseDown = function(e) {
336 | preventDefault(e);
337 | stopPropagation(e);
338 |
339 | startX = e.clientX;
340 | startY = e.clientY;
341 |
342 | var bounds = parent.getBoundingClientRect();
343 |
344 | startWidth = bounds.width;
345 | startHeight = bounds.height;
346 |
347 | domEvent.bind(document, 'mousemove', onMouseMove);
348 | domEvent.bind(document, 'mouseup', onMouseUp);
349 | };
350 |
351 | var onMouseMove = function(e) {
352 | preventDefault(e);
353 | stopPropagation(e);
354 |
355 | var newWidth = min(max(startWidth + e.clientX - startX, minWidth), maxWidth);
356 | var newHeight = min(max(startHeight + e.clientY - startY, minHeight), maxHeight);
357 |
358 | parent.style.width = newWidth + 'px';
359 | parent.style.height = newHeight + 'px';
360 |
361 | self.resizeHandler({
362 | width: startWidth,
363 | height: startHeight,
364 | dx: e.clientX - startX,
365 | dy: e.clientY - startY
366 | });
367 | };
368 |
369 | var onMouseUp = function(e) {
370 | preventDefault(e);
371 | stopPropagation(e);
372 |
373 | domEvent.unbind(document,'mousemove', onMouseMove, false);
374 | domEvent.unbind(document, 'mouseup', onMouseUp, false);
375 | };
376 |
377 | domEvent.bind(resizeHandle, 'mousedown', onMouseDown);
378 | }
379 |
380 | assign(resizeHandle.style, {
381 | position: 'absolute',
382 | bottom: '0px',
383 | right: '0px',
384 | cursor: 'nwse-resize',
385 | width: '0',
386 | height: '0',
387 | borderTop: (parseInt(this.style.fontSize) / 4 || 3) + 'px solid transparent',
388 | borderRight: (parseInt(this.style.fontSize) / 4 || 3) + 'px solid #ccc',
389 | borderBottom: (parseInt(this.style.fontSize) / 4 || 3) + 'px solid #ccc',
390 | borderLeft: (parseInt(this.style.fontSize) / 4 || 3) + 'px solid transparent'
391 | });
392 |
393 | parent.appendChild(resizeHandle);
394 | };
395 |
396 |
397 | /**
398 | * Clear content and style of the textbox, unbind listeners and
399 | * reset CSS style.
400 | */
401 | TextBox.prototype.destroy = function() {
402 | var parent = this.parent,
403 | content = this.content,
404 | resizeHandle = this.resizeHandle;
405 |
406 | // clear content
407 | content.innerText = '';
408 |
409 | // clear styles
410 | parent.removeAttribute('style');
411 | content.removeAttribute('style');
412 |
413 | domEvent.unbind(content, 'keydown', this.keyHandler);
414 | domEvent.unbind(content, 'mousedown', stopPropagation);
415 | domEvent.unbind(content, 'input', this.autoResize);
416 | domEvent.unbind(content, 'paste', this.handlePaste);
417 |
418 | if (resizeHandle) {
419 | resizeHandle.removeAttribute('style');
420 |
421 | domRemove(resizeHandle);
422 | }
423 |
424 | domRemove(parent);
425 | };
426 |
427 |
428 | TextBox.prototype.getValue = function() {
429 | return this.content.innerText.trim();
430 | };
431 |
432 |
433 | TextBox.prototype.getSelection = function() {
434 | var selection = window.getSelection(),
435 | range = selection.getRangeAt(0);
436 |
437 | return range;
438 | };
439 |
440 |
441 | TextBox.prototype.setSelection = function(container, offset) {
442 | var range = document.createRange();
443 |
444 | if (container === null) {
445 | range.selectNodeContents(this.content);
446 | } else {
447 | range.setStart(container, offset);
448 | range.setEnd(container, offset);
449 | }
450 |
451 | var selection = window.getSelection();
452 |
453 | selection.removeAllRanges();
454 | selection.addRange(range);
455 | };
456 |
457 | // helpers //////////
458 |
459 | function normalizeEndOfLineSequences(string) {
460 | return string.replace(/\r\n|\r|\n/g, '\n');
461 | }
--------------------------------------------------------------------------------
/lib/index.js:
--------------------------------------------------------------------------------
1 | import InteractionEventsModule from 'diagram-js/lib/features/interaction-events';
2 |
3 | import DirectEditing from './DirectEditing.js';
4 |
5 | export default {
6 | __depends__: [
7 | InteractionEventsModule
8 | ],
9 | __init__: [ 'directEditing' ],
10 | directEditing: [ 'type', DirectEditing ]
11 | };
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "diagram-js-direct-editing",
3 | "version": "3.2.0",
4 | "description": "Direct editing support for diagram-js",
5 | "scripts": {
6 | "all": "run-s lint test",
7 | "dev": "npm run test -- --auto-watch --no-single-run",
8 | "lint": "eslint .",
9 | "test": "karma start"
10 | },
11 | "repository": {
12 | "type": "git",
13 | "url": "https://github.com/bpmn-io/diagram-js-direct-editing"
14 | },
15 | "keywords": [
16 | "diagram-js",
17 | "diagram-js-plugin"
18 | ],
19 | "engines": {
20 | "node": "*"
21 | },
22 | "main": "lib/index.js",
23 | "module": "lib/index.js",
24 | "author": "bpmn.io",
25 | "license": "MIT",
26 | "devDependencies": {
27 | "chai": "^4.3.10",
28 | "diagram-js": "^15.0.0",
29 | "eslint": "^9.12.0",
30 | "eslint-plugin-bpmn-io": "^2.0.2",
31 | "karma": "^6.4.2",
32 | "karma-chrome-launcher": "^3.2.0",
33 | "karma-firefox-launcher": "^2.1.2",
34 | "karma-mocha": "^2.0.1",
35 | "karma-sinon-chai": "^2.0.2",
36 | "karma-webpack": "^5.0.0",
37 | "mocha": "^10.2.0",
38 | "mocha-test-container-support": "^0.2.0",
39 | "npm-run-all2": "^8.0.0",
40 | "puppeteer": "^24.0.0",
41 | "sinon": "^17.0.1",
42 | "sinon-chai": "^3.7.0",
43 | "webpack": "^5.89.0"
44 | },
45 | "dependencies": {
46 | "min-dash": "^4.0.0",
47 | "min-dom": "^4.2.1"
48 | },
49 | "peerDependencies": {
50 | "diagram-js": "*"
51 | },
52 | "files": [
53 | "lib"
54 | ]
55 | }
56 |
--------------------------------------------------------------------------------
/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json",
3 | "extends": [
4 | "github>bpmn-io/renovate-config:recommended"
5 | ]
6 | }
7 |
--------------------------------------------------------------------------------
/resources/screencast.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bpmn-io/diagram-js-direct-editing/a5e111e0ef2f14914084fdeb9178c92ef483e73b/resources/screencast.gif
--------------------------------------------------------------------------------
/test/DirectEditingProvider.js:
--------------------------------------------------------------------------------
1 | import {
2 | assign
3 | } from 'min-dash';
4 |
5 |
6 | export default function DirectEditingProvider(directEditing) {
7 | directEditing.registerProvider(this);
8 | }
9 |
10 | DirectEditingProvider.$inject = [ 'directEditing' ];
11 |
12 | DirectEditingProvider.prototype.activate = function(element) {
13 | var context = {};
14 |
15 | if (element.label) {
16 | assign(context, {
17 | bounds: element.labelBounds || element,
18 | text: element.label
19 | });
20 |
21 | assign(context, {
22 | options: this.options || {}
23 | });
24 |
25 | return context;
26 | }
27 | };
28 |
29 | DirectEditingProvider.prototype.update = function(element, text, oldText, bounds) {
30 | element.label = text;
31 |
32 | var labelBounds = element.labelBounds || element;
33 |
34 | assign(labelBounds, bounds);
35 | };
36 |
37 | DirectEditingProvider.prototype.setOptions = function(options) {
38 | this.options = options;
39 | };
40 |
--------------------------------------------------------------------------------
/test/DirectEditingSpec.js:
--------------------------------------------------------------------------------
1 | /* global sinon */
2 |
3 | import {
4 | bootstrapDiagram,
5 | inject
6 | } from 'diagram-js/test/helper';
7 |
8 | import {
9 | forEach
10 | } from 'min-dash';
11 |
12 | import directEditingModule from '..';
13 |
14 | import DirectEditingProvider from './DirectEditingProvider';
15 |
16 |
17 | var DELTA = 2;
18 |
19 |
20 | function triggerMouseEvent(element, event, clientX, clientY) {
21 | var e = document.createEvent('MouseEvent');
22 |
23 | if (e.initMouseEvent) {
24 | e.initMouseEvent(event, true, true, window, 0, 0, 0, clientX, clientY, false, false, false, false, 0, null);
25 | }
26 |
27 | element.dispatchEvent(e);
28 | }
29 |
30 | function triggerKeyEvent(element, event, code) {
31 | var e = document.createEvent('Events');
32 |
33 | if (e.initEvent) {
34 | e.initEvent(event, true, true);
35 | }
36 |
37 | e.keyCode = code;
38 | e.which = code;
39 |
40 | element.dispatchEvent(e);
41 | }
42 |
43 | function expectEditingActive(directEditing, parentBounds, contentBounds) {
44 | expect(directEditing.isActive()).to.eql(true);
45 |
46 | var parent = directEditing._textbox.parent,
47 | content = directEditing._textbox.content;
48 |
49 | expect(parent.className).to.eql('djs-direct-editing-parent');
50 | expect(content.className).to.eql('djs-direct-editing-content');
51 |
52 | forEach(parentBounds, function(val, key) {
53 | expect(parseInt(parent['offset' + key.charAt(0).toUpperCase() + key.slice(1)])).to.be.closeTo(val, DELTA);
54 | });
55 |
56 | if (contentBounds) {
57 | forEach(contentBounds, function(val, key) {
58 | expect(content['offset' + key.charAt(0).toUpperCase() + key.slice(1)]).to.be.closeTo(val, DELTA);
59 | });
60 | }
61 | }
62 |
63 |
64 | describe('diagram-js-direct-editing', function() {
65 |
66 |
67 | describe('bootstrap', function() {
68 |
69 | beforeEach(bootstrapDiagram({
70 | modules: [ directEditingModule ]
71 | }));
72 |
73 | it('should bootstrap diagram with component', inject(function() { }));
74 |
75 | });
76 |
77 |
78 | describe('behavior', function() {
79 |
80 | var providerModule = {
81 | __init__: [ 'directEditingProvider' ],
82 | __depends__: [ directEditingModule ],
83 | directEditingProvider: [ 'type', DirectEditingProvider ]
84 | };
85 |
86 | beforeEach(bootstrapDiagram({
87 | modules: [ providerModule ]
88 | }));
89 |
90 | afterEach(inject(function(directEditingProvider) {
91 | directEditingProvider.setOptions(undefined);
92 | sinon.restore();
93 | }));
94 |
95 |
96 | it('should register provider', inject(function(directEditing) {
97 | expect(directEditing._providers[0] instanceof DirectEditingProvider).to.eql(true);
98 | }));
99 |
100 |
101 | describe('controlled by provider', function() {
102 |
103 | it('should activate', inject(function(canvas, directEditing) {
104 |
105 | // given
106 | var shapeWithLabel = {
107 | id: 's1',
108 | x: 20, y: 10, width: 60, height: 50,
109 | label: 'FOO\nBAR'
110 | };
111 | canvas.addShape(shapeWithLabel);
112 |
113 | var otherShape = {
114 | id: 's2',
115 | x: 220, y: 10, width: 60, height: 50,
116 | label: 'other'
117 | };
118 | canvas.addShape(otherShape);
119 |
120 | // when
121 | var activated = directEditing.activate(shapeWithLabel);
122 |
123 | // then
124 | expect(activated).to.eql(true);
125 |
126 | expect(directEditing.isActive()).to.eql(true);
127 | expect(directEditing.isActive(shapeWithLabel)).to.eql(true);
128 | expect(directEditing.isActive(otherShape)).to.eql(false);
129 |
130 | expect(directEditing.getValue()).to.eql('FOO\nBAR');
131 |
132 | var parentBounds = {
133 | left: 20,
134 | top: 10,
135 | width: 60,
136 | height: 50
137 | };
138 |
139 | var contentBounds = {
140 | top: 0,
141 | left: 0,
142 | width: 60,
143 | height: 38
144 | };
145 |
146 | // textbox is correctly positioned
147 | expectEditingActive(directEditing, parentBounds, contentBounds);
148 | }));
149 |
150 |
151 | it('should activate with custom bounds', inject(function(canvas, directEditing) {
152 |
153 | // given
154 | var shapeWithLabel = {
155 | id: 's1',
156 | x: 20, y: 10, width: 60, height: 50,
157 | label: 'FOO',
158 | labelBounds: { x: 100, y: 100, width: 50, height: 20 }
159 | };
160 | canvas.addShape(shapeWithLabel);
161 |
162 | // when
163 | var activated = directEditing.activate(shapeWithLabel);
164 |
165 | // then
166 | expect(activated).to.eql(true);
167 |
168 | var parentBounds = { left: 100, top: 100, width: 50, height: 20 },
169 | contentBounds = { left: 0, top: 0, width: 50, height: 18 };
170 |
171 | // textbox is correctly positioned
172 | expectEditingActive(directEditing, parentBounds, contentBounds);
173 | }));
174 |
175 |
176 | it('should activate with vertically centered text', inject(function(canvas, directEditing, directEditingProvider) {
177 |
178 | // given
179 | var shapeWithLabel = {
180 | id: 's1',
181 | x: 20, y: 10, width: 60, height: 50,
182 | label: 'FOO'
183 | };
184 | canvas.addShape(shapeWithLabel);
185 |
186 | directEditingProvider.setOptions({ centerVertically: true });
187 |
188 | // when
189 | var activated = directEditing.activate(shapeWithLabel);
190 |
191 | // then
192 | expect(activated).to.eql(true);
193 | expect(directEditing.getValue()).to.eql('FOO');
194 |
195 | var parentBounds = { left: 20, top: 10, width: 60, height: 50 },
196 | contentBounds = { left: 0, top: 25, width: 60, height: 18 };
197 |
198 | // textbox is correctly positioned
199 | expectEditingActive(directEditing, parentBounds, contentBounds);
200 | }));
201 |
202 |
203 | it('should NOT activate', inject(function(canvas, directEditing) {
204 |
205 | // given
206 | var shapeNoLabel = {
207 | id: 's1',
208 | x: 20, y: 10, width: 60, height: 50
209 | };
210 | canvas.addShape(shapeNoLabel);
211 |
212 | // when
213 | var activated = directEditing.activate(shapeNoLabel);
214 |
215 | // then
216 | expect(activated).to.eql(false);
217 | expect(directEditing.isActive()).to.eql(false);
218 | expect(directEditing.isActive(shapeNoLabel)).to.eql(false);
219 | }));
220 |
221 |
222 | it('should cancel', inject(function(canvas, directEditing) {
223 |
224 | // given
225 | var shapeWithLabel = {
226 | id: 's1',
227 | x: 20, y: 10, width: 60, height: 50,
228 | label: 'FOO'
229 | };
230 | canvas.addShape(shapeWithLabel);
231 |
232 | directEditing.activate(shapeWithLabel);
233 |
234 |
235 | // when
236 | directEditing.cancel();
237 |
238 | // then
239 | expect(directEditing.isActive()).to.eql(false);
240 | expect(directEditing.isActive(shapeWithLabel)).to.eql(false);
241 |
242 | // textbox is detached (invisible)
243 | expect(directEditing._textbox.parent.parentNode).not.to.exist;
244 | }));
245 |
246 |
247 | it('should cancel via ESC', inject(function(canvas, directEditing) {
248 |
249 | // given
250 | var shapeWithLabel = {
251 | id: 's1',
252 | x: 20, y: 10, width: 60, height: 50,
253 | label: 'FOO'
254 | };
255 | canvas.addShape(shapeWithLabel);
256 |
257 | var textbox = directEditing._textbox;
258 |
259 | directEditing.activate(shapeWithLabel);
260 |
261 | // when pressing ESC
262 | triggerKeyEvent(textbox.content, 'keydown', 27);
263 |
264 | // then
265 | expect(directEditing.isActive()).to.eql(false);
266 |
267 | // textbox container is detached (invisible)
268 | expect(textbox.parent.parentNode).not.to.exist;
269 | }));
270 |
271 |
272 | it('should complete + update label via ENTER', inject(function(canvas, directEditing) {
273 |
274 | // given
275 | var shapeWithLabel = {
276 | id: 's1',
277 | x: 20, y: 10, width: 60, height: 50,
278 | label: 'FOO'
279 | };
280 | canvas.addShape(shapeWithLabel);
281 |
282 | var textbox = directEditing._textbox;
283 |
284 | directEditing.activate(shapeWithLabel);
285 |
286 | textbox.content.innerText = 'BAR';
287 |
288 | // when pressing Enter
289 | triggerKeyEvent(textbox.content, 'keydown', 13);
290 |
291 | // then
292 | expect(directEditing.isActive()).to.eql(false);
293 |
294 | // textbox is detached (invisible)
295 | expect(textbox.parent.parentNode).not.to.exist;
296 |
297 | expect(shapeWithLabel.label).to.eql('BAR');
298 | }));
299 |
300 |
301 | it('should complete with unchanged bounds', inject(function(canvas, directEditing) {
302 |
303 | var labelBounds = { x: 100, y: 200, width: 300, height: 20 };
304 |
305 | var shapeWithLabel = {
306 | id: 's1',
307 | x: 20, y: 10, width: 60, height: 50,
308 | label: 'FOO',
309 | labelBounds: labelBounds
310 | };
311 |
312 | canvas.addShape(shapeWithLabel);
313 |
314 | var textbox = directEditing._textbox;
315 |
316 | directEditing.activate(shapeWithLabel);
317 |
318 | textbox.content.innerText = 'BAR';
319 |
320 | directEditing.complete();
321 |
322 | var bounds = shapeWithLabel.labelBounds;
323 |
324 | expect(bounds).to.eql(labelBounds);
325 | }));
326 |
327 |
328 | it('should complete with changed bounds', inject(function(canvas, directEditing) {
329 |
330 | var labelBounds = { x: 100, y: 200, width: 300, height: 20 };
331 |
332 | var shapeWithLabel = {
333 | id: 's1',
334 | x: 20, y: 10, width: 60, height: 50,
335 | label: 'FOO',
336 | labelBounds: labelBounds
337 | };
338 |
339 | canvas.addShape(shapeWithLabel);
340 |
341 | var textbox = directEditing._textbox;
342 |
343 | directEditing.activate(shapeWithLabel);
344 |
345 | textbox.content.innerText = 'BAR';
346 | textbox.parent.style.left = '60px';
347 | textbox.parent.style.top = '80px';
348 | textbox.parent.style.width = '50px';
349 | textbox.parent.style.height = '100px';
350 |
351 | directEditing.complete();
352 |
353 | var bounds = shapeWithLabel.labelBounds;
354 |
355 | expect(bounds).to.eql({
356 | x: 60, y: 80, width: 50, height: 100
357 | });
358 | }));
359 |
360 |
361 | it('should update with changed text', inject(
362 | function(canvas, directEditing, directEditingProvider) {
363 |
364 | // given
365 | sinon.spy(directEditingProvider, 'update');
366 |
367 | var shapeWithLabel = {
368 | id: 's1',
369 | x: 20, y: 10, width: 60, height: 50,
370 | label: 'FOO',
371 | labelBounds: { x: 100, y: 200, width: 300, height: 20 }
372 | };
373 |
374 | canvas.addShape(shapeWithLabel);
375 |
376 | // when
377 | directEditing.activate(shapeWithLabel);
378 |
379 | var textbox = directEditing._textbox;
380 |
381 | textbox.content.innerText = 'BAR';
382 |
383 | directEditing.complete();
384 |
385 | // then
386 | expect(directEditingProvider.update).to.have.been.calledOnce;
387 | }
388 | ));
389 |
390 |
391 | it('should update with changed bounds height', inject(
392 | function(canvas, directEditing, directEditingProvider) {
393 |
394 | // given
395 | sinon.spy(directEditingProvider, 'update');
396 |
397 | var shapeWithLabel = {
398 | id: 's1',
399 | x: 20, y: 10, width: 60, height: 50,
400 | label: 'FOO',
401 | labelBounds: { x: 100, y: 200, width: 300, height: 20 }
402 | };
403 |
404 | canvas.addShape(shapeWithLabel);
405 |
406 | // when
407 | directEditing.activate(shapeWithLabel);
408 |
409 | var textbox = directEditing._textbox;
410 |
411 | textbox.parent.style.height = '21px';
412 |
413 | directEditing.complete();
414 |
415 | // then
416 | expect(directEditingProvider.update).to.have.been.calledOnce;
417 | }
418 | ));
419 |
420 |
421 | it('should update with changed bounds width', inject(
422 | function(canvas, directEditing, directEditingProvider) {
423 |
424 | // given
425 | sinon.spy(directEditingProvider, 'update');
426 |
427 | var shapeWithLabel = {
428 | id: 's1',
429 | x: 20, y: 10, width: 60, height: 50,
430 | label: 'FOO',
431 | labelBounds: { x: 100, y: 200, width: 300, height: 20 }
432 | };
433 |
434 | canvas.addShape(shapeWithLabel);
435 |
436 | // when
437 | directEditing.activate(shapeWithLabel);
438 |
439 | var textbox = directEditing._textbox;
440 |
441 | textbox.parent.style.width = '301px';
442 |
443 | directEditing.complete();
444 |
445 | // then
446 | expect(directEditingProvider.update).to.have.been.calledOnce;
447 | }
448 | ));
449 |
450 |
451 | it('should NOT update with unchanged text or bounds height/width', inject(
452 | function(canvas, directEditing, directEditingProvider) {
453 |
454 | // given
455 | sinon.spy(directEditingProvider, 'update');
456 |
457 | var shapeWithLabel = {
458 | id: 's1',
459 | x: 20, y: 10, width: 60, height: 50,
460 | label: 'FOO',
461 | labelBounds: { x: 100, y: 200, width: 300, height: 20 }
462 | };
463 |
464 | canvas.addShape(shapeWithLabel);
465 |
466 | // when
467 | directEditing.activate(shapeWithLabel);
468 |
469 | directEditing.complete();
470 |
471 | // then
472 | expect(directEditingProvider.update).to.not.have.been.called;
473 | }
474 | ));
475 |
476 | });
477 |
478 |
479 | describe('textbox', function() {
480 |
481 | it('should init label on open', inject(function(canvas, directEditing) {
482 |
483 | // given
484 | var shape = {
485 | id: 's1',
486 | x: 20, y: 10, width: 60, height: 50,
487 | label: 'FOO'
488 | };
489 | canvas.addShape(shape);
490 |
491 | // when
492 | directEditing.activate(shape);
493 |
494 | // then
495 | expect(directEditing._textbox.content.innerText).to.eql('FOO');
496 |
497 | }));
498 |
499 |
500 | it('should clear label after close', inject(function(canvas, directEditing) {
501 |
502 | // given
503 | var shape = {
504 | id: 's1',
505 | x: 20, y: 10, width: 60, height: 50,
506 | label: 'FOO'
507 | };
508 | canvas.addShape(shape);
509 |
510 | // when
511 | directEditing.activate(shape);
512 | directEditing.cancel();
513 |
514 | // then
515 | expect(directEditing._textbox.content.innerText).to.eql('');
516 | }));
517 |
518 |
519 | it('should trim label when getting value', inject(function(canvas, directEditing) {
520 |
521 | // given
522 | var shape = {
523 | id: 's1',
524 | x: 20, y: 10, width: 60, height: 50,
525 | label: '\nFOO\n'
526 | };
527 | canvas.addShape(shape);
528 |
529 | // when
530 | directEditing.activate(shape);
531 |
532 | // then
533 | expect(directEditing.getValue()).to.eql('FOO');
534 | }));
535 |
536 |
537 | it('should show resize handle if resizable', inject(function(canvas, directEditing, directEditingProvider) {
538 |
539 | // given
540 | var shape = {
541 | id: 's1',
542 | x: 20, y: 10, width: 60, height: 50,
543 | label: 'FOO'
544 | };
545 | canvas.addShape(shape);
546 |
547 | directEditingProvider.setOptions({ resizable: true });
548 |
549 | // when
550 | directEditing.activate(shape);
551 |
552 | // then
553 | var resizeHandle = directEditing._textbox.parent.getElementsByClassName('djs-direct-editing-resize-handle')[0];
554 |
555 | expect(resizeHandle).to.exist;
556 | }));
557 |
558 |
559 | it('should not show resize handle if not resizable', inject(function(canvas, directEditing) {
560 |
561 | // given
562 | var shape = {
563 | id: 's1',
564 | x: 20, y: 10, width: 60, height: 50,
565 | label: 'FOO'
566 | };
567 | canvas.addShape(shape);
568 |
569 | // when
570 | directEditing.activate(shape);
571 |
572 | // then
573 | var resizeHandle = directEditing._textbox.parent.getElementsByClassName('djs-direct-editing-resize-handle')[0];
574 |
575 | expect(resizeHandle).not.to.exist;
576 | }));
577 |
578 |
579 | it('should resize automatically', inject(function(canvas, directEditing, directEditingProvider) {
580 |
581 | // given
582 | var shape = {
583 | id: 's1',
584 | x: 20, y: 10, width: 60, height: 50,
585 | label: 'FOO\nBAR\nBAZ\nFOO\nBAR\nBAZ'
586 | };
587 | canvas.addShape(shape);
588 |
589 | directEditingProvider.setOptions({ autoResize: true });
590 |
591 | directEditing.activate(shape);
592 |
593 | var parent = directEditing._textbox.parent,
594 | content = directEditing._textbox.content;
595 |
596 | // when
597 | triggerKeyEvent(directEditing._textbox.content, 'input', 65);
598 |
599 | // then
600 | var parentHeight = parent.getBoundingClientRect().height,
601 | contentScrollHeight = content.scrollHeight;
602 |
603 | expect(parentHeight).to.be.closeTo(contentScrollHeight, DELTA);
604 | }));
605 |
606 |
607 | it('should resize on resize handle drag', inject(function(canvas, directEditing, directEditingProvider) {
608 |
609 | // given
610 | var shape = {
611 | id: 's1',
612 | x: 20, y: 10, width: 60, height: 50,
613 | label: 'FOO'
614 | };
615 | canvas.addShape(shape);
616 |
617 | directEditingProvider.setOptions({ resizable: true });
618 |
619 | directEditing.activate(shape);
620 |
621 | var parent = directEditing._textbox.parent,
622 | resizeHandle = directEditing._textbox.resizeHandle;
623 |
624 | // when
625 | var oldParentBounds = parent.getBoundingClientRect(),
626 | resizeHandleBounds = resizeHandle.getBoundingClientRect();
627 |
628 | var clientX = resizeHandleBounds.left + resizeHandleBounds.width / 2,
629 | clientY = resizeHandleBounds.top + resizeHandleBounds.height / 2;
630 |
631 | triggerMouseEvent(resizeHandle, 'mousedown', clientX, clientY);
632 | triggerMouseEvent(resizeHandle, 'mousemove', clientX + 100, clientY + 100);
633 | triggerMouseEvent(resizeHandle, 'mouseup', clientX + 100, clientY + 100);
634 |
635 | // then
636 | var newParentBounds = parent.getBoundingClientRect();
637 |
638 | expect(newParentBounds.width).to.be.closeTo(oldParentBounds.width + 100, DELTA);
639 | expect(newParentBounds.height).to.be.closeTo(oldParentBounds.height + 100, DELTA);
640 | }));
641 |
642 |
643 | it('should not insert HTML', inject(function(canvas, directEditing) {
644 |
645 | // given
646 | var shape = {
647 | id: 's1',
648 | x: 20, y: 10, width: 60, height: 50,
649 | label: 'FOO'
650 | };
651 | canvas.addShape(shape);
652 |
653 | directEditing.activate(shape);
654 |
655 | var textBox = directEditing._textbox;
656 |
657 | // when
658 | textBox.insertText('