├── .gitignore
├── resources
└── screencast.gif
├── test
├── globals.js
├── suite.js
├── DirectEditingProvider.js
└── DirectEditingSpec.js
├── renovate.json
├── lib
├── index.js
├── DirectEditing.js
└── TextBox.js
├── README.md
├── eslint.config.mjs
├── .github
└── workflows
│ └── CI.yml
├── karma.conf.js
├── LICENSE
├── package.json
└── CHANGELOG.md
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | .idea/
--------------------------------------------------------------------------------
/resources/screencast.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bpmn-io/diagram-js-direct-editing/HEAD/resources/screencast.gif
--------------------------------------------------------------------------------
/test/globals.js:
--------------------------------------------------------------------------------
1 | import {
2 | use
3 | } from 'chai';
4 |
5 | import sinonChai from 'sinon-chai';
6 |
7 | use(sinonChai);
8 |
--------------------------------------------------------------------------------
/test/suite.js:
--------------------------------------------------------------------------------
1 | /* global require */
2 |
3 | import('./globals');
4 |
5 | var allTests = require.context('.', true, /Spec\.js$/);
6 |
7 | allTests.keys().forEach(allTests);
--------------------------------------------------------------------------------
/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json",
3 | "extends": [
4 | "github>bpmn-io/renovate-config:recommended"
5 | ]
6 | }
7 |
--------------------------------------------------------------------------------
/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 | };
--------------------------------------------------------------------------------
/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 | ];
--------------------------------------------------------------------------------
/.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@v6
16 | - name: Use Node.js 24
17 | uses: actions/setup-node@v6
18 | with:
19 | node-version: 24
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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/karma.conf.js:
--------------------------------------------------------------------------------
1 | // configures browsers to run test against
2 | // any of [ 'ChromeHeadless', 'Chrome', 'Firefox' ]
3 | const browsers = (process.env.TEST_BROWSERS || 'ChromeHeadless').split(',');
4 |
5 | // use puppeteer provided Chrome for testing
6 | process.env.CHROME_BIN = require('puppeteer').executablePath();
7 |
8 |
9 | module.exports = function(karma) {
10 |
11 | karma.set({
12 | frameworks: [
13 | 'webpack',
14 | 'mocha'
15 | ],
16 |
17 | files: [
18 | 'test/suite.js'
19 | ],
20 |
21 | preprocessors: {
22 | 'test/suite.js': [ 'webpack' ]
23 | },
24 |
25 | reporters: [ 'progress' ],
26 |
27 | browsers,
28 |
29 | singleRun: true,
30 | autoWatch: false,
31 |
32 | webpack: {
33 | mode: 'development',
34 | module: {
35 | rules: [
36 | {
37 | test: require.resolve('./test/globals.js'),
38 | sideEffects: true
39 | }
40 | ]
41 | }
42 | }
43 | });
44 |
45 | };
46 |
--------------------------------------------------------------------------------
/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.
--------------------------------------------------------------------------------
/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": "^6.2.2",
28 | "diagram-js": "^15.0.0",
29 | "eslint": "^9.39.2",
30 | "eslint-plugin-bpmn-io": "^2.2.0",
31 | "karma": "^6.4.4",
32 | "karma-chrome-launcher": "^3.2.0",
33 | "karma-firefox-launcher": "^2.1.3",
34 | "karma-mocha": "^2.0.1",
35 | "karma-webpack": "^5.0.1",
36 | "mocha": "^11.0.0",
37 | "mocha-test-container-support": "^0.2.0",
38 | "npm-run-all2": "^8.0.4",
39 | "puppeteer": "^24.34.0",
40 | "sinon": "^21.0.0",
41 | "sinon-chai": "^4.0.0",
42 | "webpack": "^5.104.1"
43 | },
44 | "dependencies": {
45 | "min-dash": "^4.0.0",
46 | "min-dom": "^4.2.1"
47 | },
48 | "peerDependencies": {
49 | "diagram-js": "*"
50 | },
51 | "files": [
52 | "lib"
53 | ]
54 | }
55 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/test/DirectEditingSpec.js:
--------------------------------------------------------------------------------
1 | import { expect } from 'chai';
2 | import { spy, restore } from 'sinon';
3 |
4 | import {
5 | bootstrapDiagram,
6 | inject
7 | } from 'diagram-js/test/helper';
8 |
9 | import {
10 | forEach
11 | } from 'min-dash';
12 |
13 | import directEditingModule from '..';
14 |
15 | import DirectEditingProvider from './DirectEditingProvider';
16 |
17 |
18 | var DELTA = 2;
19 |
20 |
21 | function triggerMouseEvent(element, event, clientX, clientY) {
22 | var e = document.createEvent('MouseEvent');
23 |
24 | if (e.initMouseEvent) {
25 | e.initMouseEvent(event, true, true, window, 0, 0, 0, clientX, clientY, false, false, false, false, 0, null);
26 | }
27 |
28 | element.dispatchEvent(e);
29 | }
30 |
31 | function triggerKeyEvent(element, event, code) {
32 | var e = document.createEvent('Events');
33 |
34 | if (e.initEvent) {
35 | e.initEvent(event, true, true);
36 | }
37 |
38 | e.keyCode = code;
39 | e.which = code;
40 |
41 | element.dispatchEvent(e);
42 | }
43 |
44 | function expectEditingActive(directEditing, parentBounds, contentBounds) {
45 | expect(directEditing.isActive()).to.eql(true);
46 |
47 | var parent = directEditing._textbox.parent,
48 | content = directEditing._textbox.content;
49 |
50 | expect(parent.className).to.eql('djs-direct-editing-parent');
51 | expect(content.className).to.eql('djs-direct-editing-content');
52 |
53 | forEach(parentBounds, function(val, key) {
54 | expect(parseInt(parent['offset' + key.charAt(0).toUpperCase() + key.slice(1)])).to.be.closeTo(val, DELTA);
55 | });
56 |
57 | if (contentBounds) {
58 | forEach(contentBounds, function(val, key) {
59 | expect(content['offset' + key.charAt(0).toUpperCase() + key.slice(1)]).to.be.closeTo(val, DELTA);
60 | });
61 | }
62 | }
63 |
64 |
65 | describe('diagram-js-direct-editing', function() {
66 |
67 |
68 | describe('bootstrap', function() {
69 |
70 | beforeEach(bootstrapDiagram({
71 | modules: [ directEditingModule ]
72 | }));
73 |
74 | it('should bootstrap diagram with component', inject(function() { }));
75 |
76 | });
77 |
78 |
79 | describe('behavior', function() {
80 |
81 | var providerModule = {
82 | __init__: [ 'directEditingProvider' ],
83 | __depends__: [ directEditingModule ],
84 | directEditingProvider: [ 'type', DirectEditingProvider ]
85 | };
86 |
87 | beforeEach(bootstrapDiagram({
88 | modules: [ providerModule ]
89 | }));
90 |
91 | afterEach(inject(function(directEditingProvider) {
92 | directEditingProvider.setOptions(undefined);
93 |
94 | restore();
95 | }));
96 |
97 |
98 | it('should register provider', inject(function(directEditing) {
99 | expect(directEditing._providers[0] instanceof DirectEditingProvider).to.eql(true);
100 | }));
101 |
102 |
103 | describe('controlled by provider', function() {
104 |
105 | it('should activate', inject(function(canvas, directEditing) {
106 |
107 | // given
108 | var shapeWithLabel = {
109 | id: 's1',
110 | x: 20, y: 10, width: 60, height: 50,
111 | label: 'FOO\nBAR'
112 | };
113 | canvas.addShape(shapeWithLabel);
114 |
115 | var otherShape = {
116 | id: 's2',
117 | x: 220, y: 10, width: 60, height: 50,
118 | label: 'other'
119 | };
120 | canvas.addShape(otherShape);
121 |
122 | // when
123 | var activated = directEditing.activate(shapeWithLabel);
124 |
125 | // then
126 | expect(activated).to.eql(true);
127 |
128 | expect(directEditing.isActive()).to.eql(true);
129 | expect(directEditing.isActive(shapeWithLabel)).to.eql(true);
130 | expect(directEditing.isActive(otherShape)).to.eql(false);
131 |
132 | expect(directEditing.getValue()).to.eql('FOO\nBAR');
133 |
134 | var parentBounds = {
135 | left: 20,
136 | top: 10,
137 | width: 60,
138 | height: 50
139 | };
140 |
141 | var contentBounds = {
142 | top: 0,
143 | left: 0,
144 | width: 60,
145 | height: 38
146 | };
147 |
148 | // textbox is correctly positioned
149 | expectEditingActive(directEditing, parentBounds, contentBounds);
150 | }));
151 |
152 |
153 | it('should activate with custom bounds', inject(function(canvas, directEditing) {
154 |
155 | // given
156 | var shapeWithLabel = {
157 | id: 's1',
158 | x: 20, y: 10, width: 60, height: 50,
159 | label: 'FOO',
160 | labelBounds: { x: 100, y: 100, width: 50, height: 20 }
161 | };
162 | canvas.addShape(shapeWithLabel);
163 |
164 | // when
165 | var activated = directEditing.activate(shapeWithLabel);
166 |
167 | // then
168 | expect(activated).to.eql(true);
169 |
170 | var parentBounds = { left: 100, top: 100, width: 50, height: 20 },
171 | contentBounds = { left: 0, top: 0, width: 50, height: 18 };
172 |
173 | // textbox is correctly positioned
174 | expectEditingActive(directEditing, parentBounds, contentBounds);
175 | }));
176 |
177 |
178 | it('should activate with vertically centered text', inject(function(canvas, directEditing, directEditingProvider) {
179 |
180 | // given
181 | var shapeWithLabel = {
182 | id: 's1',
183 | x: 20, y: 10, width: 60, height: 50,
184 | label: 'FOO'
185 | };
186 | canvas.addShape(shapeWithLabel);
187 |
188 | directEditingProvider.setOptions({ centerVertically: true });
189 |
190 | // when
191 | var activated = directEditing.activate(shapeWithLabel);
192 |
193 | // then
194 | expect(activated).to.eql(true);
195 | expect(directEditing.getValue()).to.eql('FOO');
196 |
197 | var parentBounds = { left: 20, top: 10, width: 60, height: 50 },
198 | contentBounds = { left: 0, top: 25, width: 60, height: 18 };
199 |
200 | // textbox is correctly positioned
201 | expectEditingActive(directEditing, parentBounds, contentBounds);
202 | }));
203 |
204 |
205 | it('should NOT activate', inject(function(canvas, directEditing) {
206 |
207 | // given
208 | var shapeNoLabel = {
209 | id: 's1',
210 | x: 20, y: 10, width: 60, height: 50
211 | };
212 | canvas.addShape(shapeNoLabel);
213 |
214 | // when
215 | var activated = directEditing.activate(shapeNoLabel);
216 |
217 | // then
218 | expect(activated).to.eql(false);
219 | expect(directEditing.isActive()).to.eql(false);
220 | expect(directEditing.isActive(shapeNoLabel)).to.eql(false);
221 | }));
222 |
223 |
224 | it('should cancel', inject(function(canvas, directEditing) {
225 |
226 | // given
227 | var shapeWithLabel = {
228 | id: 's1',
229 | x: 20, y: 10, width: 60, height: 50,
230 | label: 'FOO'
231 | };
232 | canvas.addShape(shapeWithLabel);
233 |
234 | directEditing.activate(shapeWithLabel);
235 |
236 |
237 | // when
238 | directEditing.cancel();
239 |
240 | // then
241 | expect(directEditing.isActive()).to.eql(false);
242 | expect(directEditing.isActive(shapeWithLabel)).to.eql(false);
243 |
244 | // textbox is detached (invisible)
245 | expect(directEditing._textbox.parent.parentNode).not.to.exist;
246 | }));
247 |
248 |
249 | it('should cancel via ESC', inject(function(canvas, directEditing) {
250 |
251 | // given
252 | var shapeWithLabel = {
253 | id: 's1',
254 | x: 20, y: 10, width: 60, height: 50,
255 | label: 'FOO'
256 | };
257 | canvas.addShape(shapeWithLabel);
258 |
259 | var textbox = directEditing._textbox;
260 |
261 | directEditing.activate(shapeWithLabel);
262 |
263 | // when pressing ESC
264 | triggerKeyEvent(textbox.content, 'keydown', 27);
265 |
266 | // then
267 | expect(directEditing.isActive()).to.eql(false);
268 |
269 | // textbox container is detached (invisible)
270 | expect(textbox.parent.parentNode).not.to.exist;
271 | }));
272 |
273 |
274 | it('should complete + update label via ENTER', inject(function(canvas, directEditing) {
275 |
276 | // given
277 | var shapeWithLabel = {
278 | id: 's1',
279 | x: 20, y: 10, width: 60, height: 50,
280 | label: 'FOO'
281 | };
282 | canvas.addShape(shapeWithLabel);
283 |
284 | var textbox = directEditing._textbox;
285 |
286 | directEditing.activate(shapeWithLabel);
287 |
288 | textbox.content.innerText = 'BAR';
289 |
290 | // when pressing Enter
291 | triggerKeyEvent(textbox.content, 'keydown', 13);
292 |
293 | // then
294 | expect(directEditing.isActive()).to.eql(false);
295 |
296 | // textbox is detached (invisible)
297 | expect(textbox.parent.parentNode).not.to.exist;
298 |
299 | expect(shapeWithLabel.label).to.eql('BAR');
300 | }));
301 |
302 |
303 | it('should complete with unchanged bounds', inject(function(canvas, directEditing) {
304 |
305 | var labelBounds = { x: 100, y: 200, width: 300, height: 20 };
306 |
307 | var shapeWithLabel = {
308 | id: 's1',
309 | x: 20, y: 10, width: 60, height: 50,
310 | label: 'FOO',
311 | labelBounds: labelBounds
312 | };
313 |
314 | canvas.addShape(shapeWithLabel);
315 |
316 | var textbox = directEditing._textbox;
317 |
318 | directEditing.activate(shapeWithLabel);
319 |
320 | textbox.content.innerText = 'BAR';
321 |
322 | directEditing.complete();
323 |
324 | var bounds = shapeWithLabel.labelBounds;
325 |
326 | expect(bounds).to.eql(labelBounds);
327 | }));
328 |
329 |
330 | it('should complete with changed bounds', inject(function(canvas, directEditing) {
331 |
332 | var labelBounds = { x: 100, y: 200, width: 300, height: 20 };
333 |
334 | var shapeWithLabel = {
335 | id: 's1',
336 | x: 20, y: 10, width: 60, height: 50,
337 | label: 'FOO',
338 | labelBounds: labelBounds
339 | };
340 |
341 | canvas.addShape(shapeWithLabel);
342 |
343 | var textbox = directEditing._textbox;
344 |
345 | directEditing.activate(shapeWithLabel);
346 |
347 | textbox.content.innerText = 'BAR';
348 | textbox.parent.style.left = '60px';
349 | textbox.parent.style.top = '80px';
350 | textbox.parent.style.width = '50px';
351 | textbox.parent.style.height = '100px';
352 |
353 | directEditing.complete();
354 |
355 | var bounds = shapeWithLabel.labelBounds;
356 |
357 | expect(bounds).to.eql({
358 | x: 60, y: 80, width: 50, height: 100
359 | });
360 | }));
361 |
362 |
363 | it('should update with changed text', inject(
364 | function(canvas, directEditing, directEditingProvider) {
365 |
366 | // given
367 | var updateSpy = spy(directEditingProvider, 'update');
368 |
369 | var shapeWithLabel = {
370 | id: 's1',
371 | x: 20, y: 10, width: 60, height: 50,
372 | label: 'FOO',
373 | labelBounds: { x: 100, y: 200, width: 300, height: 20 }
374 | };
375 |
376 | canvas.addShape(shapeWithLabel);
377 |
378 | // when
379 | directEditing.activate(shapeWithLabel);
380 |
381 | var textbox = directEditing._textbox;
382 |
383 | textbox.content.innerText = 'BAR';
384 |
385 | directEditing.complete();
386 |
387 | // then
388 | expect(updateSpy).to.have.been.calledOnce;
389 | }
390 | ));
391 |
392 |
393 | it('should update with changed bounds height', inject(
394 | function(canvas, directEditing, directEditingProvider) {
395 |
396 | // given
397 | var updateSpy = spy(directEditingProvider, 'update');
398 |
399 | var shapeWithLabel = {
400 | id: 's1',
401 | x: 20, y: 10, width: 60, height: 50,
402 | label: 'FOO',
403 | labelBounds: { x: 100, y: 200, width: 300, height: 20 }
404 | };
405 |
406 | canvas.addShape(shapeWithLabel);
407 |
408 | // when
409 | directEditing.activate(shapeWithLabel);
410 |
411 | var textbox = directEditing._textbox;
412 |
413 | textbox.parent.style.height = '21px';
414 |
415 | directEditing.complete();
416 |
417 | // then
418 | expect(updateSpy).to.have.been.calledOnce;
419 | }
420 | ));
421 |
422 |
423 | it('should update with changed bounds width', inject(
424 | function(canvas, directEditing, directEditingProvider) {
425 |
426 | // given
427 | var updateSpy = spy(directEditingProvider, 'update');
428 |
429 | var shapeWithLabel = {
430 | id: 's1',
431 | x: 20, y: 10, width: 60, height: 50,
432 | label: 'FOO',
433 | labelBounds: { x: 100, y: 200, width: 300, height: 20 }
434 | };
435 |
436 | canvas.addShape(shapeWithLabel);
437 |
438 | // when
439 | directEditing.activate(shapeWithLabel);
440 |
441 | var textbox = directEditing._textbox;
442 |
443 | textbox.parent.style.width = '301px';
444 |
445 | directEditing.complete();
446 |
447 | // then
448 | expect(updateSpy).to.have.been.calledOnce;
449 | }
450 | ));
451 |
452 |
453 | it('should NOT update with unchanged text or bounds height/width', inject(
454 | function(canvas, directEditing, directEditingProvider) {
455 |
456 | // given
457 | var updateSpy = spy(directEditingProvider, 'update');
458 |
459 | var shapeWithLabel = {
460 | id: 's1',
461 | x: 20, y: 10, width: 60, height: 50,
462 | label: 'FOO',
463 | labelBounds: { x: 100, y: 200, width: 300, height: 20 }
464 | };
465 |
466 | canvas.addShape(shapeWithLabel);
467 |
468 | // when
469 | directEditing.activate(shapeWithLabel);
470 |
471 | directEditing.complete();
472 |
473 | // then
474 | expect(updateSpy).to.not.have.been.called;
475 | }
476 | ));
477 |
478 | });
479 |
480 |
481 | describe('textbox', function() {
482 |
483 | it('should init label on open', inject(function(canvas, directEditing) {
484 |
485 | // given
486 | var shape = {
487 | id: 's1',
488 | x: 20, y: 10, width: 60, height: 50,
489 | label: 'FOO'
490 | };
491 | canvas.addShape(shape);
492 |
493 | // when
494 | directEditing.activate(shape);
495 |
496 | // then
497 | expect(directEditing._textbox.content.innerText).to.eql('FOO');
498 |
499 | }));
500 |
501 |
502 | it('should clear label after close', inject(function(canvas, directEditing) {
503 |
504 | // given
505 | var shape = {
506 | id: 's1',
507 | x: 20, y: 10, width: 60, height: 50,
508 | label: 'FOO'
509 | };
510 | canvas.addShape(shape);
511 |
512 | // when
513 | directEditing.activate(shape);
514 | directEditing.cancel();
515 |
516 | // then
517 | expect(directEditing._textbox.content.innerText).to.eql('');
518 | }));
519 |
520 |
521 | it('should trim label when getting value', inject(function(canvas, directEditing) {
522 |
523 | // given
524 | var shape = {
525 | id: 's1',
526 | x: 20, y: 10, width: 60, height: 50,
527 | label: '\nFOO\n'
528 | };
529 | canvas.addShape(shape);
530 |
531 | // when
532 | directEditing.activate(shape);
533 |
534 | // then
535 | expect(directEditing.getValue()).to.eql('FOO');
536 | }));
537 |
538 |
539 | it('should show resize handle if resizable', inject(function(canvas, directEditing, directEditingProvider) {
540 |
541 | // given
542 | var shape = {
543 | id: 's1',
544 | x: 20, y: 10, width: 60, height: 50,
545 | label: 'FOO'
546 | };
547 | canvas.addShape(shape);
548 |
549 | directEditingProvider.setOptions({ resizable: true });
550 |
551 | // when
552 | directEditing.activate(shape);
553 |
554 | // then
555 | var resizeHandle = directEditing._textbox.parent.getElementsByClassName('djs-direct-editing-resize-handle')[0];
556 |
557 | expect(resizeHandle).to.exist;
558 | }));
559 |
560 |
561 | it('should not show resize handle if not resizable', inject(function(canvas, directEditing) {
562 |
563 | // given
564 | var shape = {
565 | id: 's1',
566 | x: 20, y: 10, width: 60, height: 50,
567 | label: 'FOO'
568 | };
569 | canvas.addShape(shape);
570 |
571 | // when
572 | directEditing.activate(shape);
573 |
574 | // then
575 | var resizeHandle = directEditing._textbox.parent.getElementsByClassName('djs-direct-editing-resize-handle')[0];
576 |
577 | expect(resizeHandle).not.to.exist;
578 | }));
579 |
580 |
581 | it('should resize automatically', inject(function(canvas, directEditing, directEditingProvider) {
582 |
583 | // given
584 | var shape = {
585 | id: 's1',
586 | x: 20, y: 10, width: 60, height: 50,
587 | label: 'FOO\nBAR\nBAZ\nFOO\nBAR\nBAZ'
588 | };
589 | canvas.addShape(shape);
590 |
591 | directEditingProvider.setOptions({ autoResize: true });
592 |
593 | directEditing.activate(shape);
594 |
595 | var parent = directEditing._textbox.parent,
596 | content = directEditing._textbox.content;
597 |
598 | // when
599 | triggerKeyEvent(directEditing._textbox.content, 'input', 65);
600 |
601 | // then
602 | var parentHeight = parent.getBoundingClientRect().height,
603 | contentScrollHeight = content.scrollHeight;
604 |
605 | expect(parentHeight).to.be.closeTo(contentScrollHeight, DELTA);
606 | }));
607 |
608 |
609 | it('should resize on resize handle drag', inject(function(canvas, directEditing, directEditingProvider) {
610 |
611 | // given
612 | var shape = {
613 | id: 's1',
614 | x: 20, y: 10, width: 60, height: 50,
615 | label: 'FOO'
616 | };
617 | canvas.addShape(shape);
618 |
619 | directEditingProvider.setOptions({ resizable: true });
620 |
621 | directEditing.activate(shape);
622 |
623 | var parent = directEditing._textbox.parent,
624 | resizeHandle = directEditing._textbox.resizeHandle;
625 |
626 | // when
627 | var oldParentBounds = parent.getBoundingClientRect(),
628 | resizeHandleBounds = resizeHandle.getBoundingClientRect();
629 |
630 | var clientX = resizeHandleBounds.left + resizeHandleBounds.width / 2,
631 | clientY = resizeHandleBounds.top + resizeHandleBounds.height / 2;
632 |
633 | triggerMouseEvent(resizeHandle, 'mousedown', clientX, clientY);
634 | triggerMouseEvent(resizeHandle, 'mousemove', clientX + 100, clientY + 100);
635 | triggerMouseEvent(resizeHandle, 'mouseup', clientX + 100, clientY + 100);
636 |
637 | // then
638 | var newParentBounds = parent.getBoundingClientRect();
639 |
640 | expect(newParentBounds.width).to.be.closeTo(oldParentBounds.width + 100, DELTA);
641 | expect(newParentBounds.height).to.be.closeTo(oldParentBounds.height + 100, DELTA);
642 | }));
643 |
644 |
645 | it('should not insert HTML', inject(function(canvas, directEditing) {
646 |
647 | // given
648 | var shape = {
649 | id: 's1',
650 | x: 20, y: 10, width: 60, height: 50,
651 | label: 'FOO'
652 | };
653 | canvas.addShape(shape);
654 |
655 | directEditing.activate(shape);
656 |
657 | var textBox = directEditing._textbox;
658 |
659 | // when
660 | textBox.insertText('