From 11165e2c217f7c22cae6e6693f84f4e562973170 Mon Sep 17 00:00:00 2001 From: hoferch91 Date: Tue, 15 Mar 2016 15:03:11 +0100 Subject: [PATCH] fix(textarea): support automatic-resizing textarea Closes #472 --- .../label-editing/LabelEditingProvider.js | 62 +++- .../label-editing/cmd/UpdateLabelHandler.js | 28 +- lib/features/modeling/Modeling.js | 5 +- .../label-editing/LabelEditingProviderSpec.js | 321 +++++++++++++++--- .../spec/features/modeling/UpdateLabelSpec.js | 42 ++- .../features/snapping/BpmnSnappingSpec.js | 6 +- 6 files changed, 392 insertions(+), 72 deletions(-) diff --git a/lib/features/label-editing/LabelEditingProvider.js b/lib/features/label-editing/LabelEditingProvider.js index 9b6a7319..0ff5d8af 100644 --- a/lib/features/label-editing/LabelEditingProvider.js +++ b/lib/features/label-editing/LabelEditingProvider.js @@ -4,8 +4,11 @@ var UpdateLabelHandler = require('./cmd/UpdateLabelHandler'); var LabelUtil = require('./LabelUtil'); +var minBoundsLabel = require('../../util/LabelUtil').DEFAULT_LABEL_SIZE; + var is = require('../../util/ModelUtil').is, - isExpanded = require('../../util/DiUtil').isExpanded; + isExpanded = require('../../util/DiUtil').isExpanded, + assign = require('lodash/object/assign'); var MIN_BOUNDS = { @@ -14,7 +17,8 @@ var MIN_BOUNDS = { }; -function LabelEditingProvider(eventBus, canvas, directEditing, commandStack) { + +function LabelEditingProvider(eventBus, canvas, directEditing, commandStack, elementFactory) { directEditing.registerProvider(this); commandStack.registerHandler('element.updateLabel', UpdateLabelHandler); @@ -64,9 +68,10 @@ function LabelEditingProvider(eventBus, canvas, directEditing, commandStack) { this._canvas = canvas; this._commandStack = commandStack; + this._elementFactory = elementFactory; } -LabelEditingProvider.$inject = [ 'eventBus', 'canvas', 'directEditing', 'commandStack' ]; +LabelEditingProvider.$inject = [ 'eventBus', 'canvas', 'directEditing', 'commandStack', 'elementFactory' ]; module.exports = LabelEditingProvider; @@ -80,7 +85,7 @@ LabelEditingProvider.prototype.activate = function(element) { } var bbox = this.getEditingBBox(element); - + var options = {}; // adjust for expanded pools AND lanes if ((is(element, 'bpmn:Participant') && isExpanded(element)) || is(element, 'bpmn:Lane')) { @@ -115,14 +120,28 @@ LabelEditingProvider.prototype.activate = function(element) { bbox.x = bbox.mid.x - element.width / 2; } - return { bounds: bbox, text: text }; + // autosizing for TextAnnotation + if (is(element, 'bpmn:TextAnnotation')) { + options.autosizing = true; + options.textAlignment = 'left'; + options.defaultHeight = this._elementFactory._getDefaultSize(element).height; + options.maxHeight = 100; + } + + // and external label + if(element.label || element.type === 'label') { + options.autosizing = true; + options.defaultHeight = 50; + options.maxHeight = 100; + } + + return { bounds: bbox, text: text, options: options }; }; LabelEditingProvider.prototype.getEditingBBox = function(element, maxBounds) { var target = element.label || element; - var bbox = this._canvas.getAbsoluteBBox(target); var mid = { @@ -143,10 +162,37 @@ LabelEditingProvider.prototype.getEditingBBox = function(element, maxBounds) { return bbox; }; +LabelEditingProvider.prototype.update = function(element, newLabel, newSize) { + var newBounds = {}; + + var target = element.label || element; + + if(is(target, 'bpmn:TextAnnotation') || target.type === 'label'){ + var newX = null; + if (target.type === 'label') { + // newSize-obj carries dimensions of the textarea which have to be adapted + newSize.width = newSize.width <= MIN_BOUNDS.width ? minBoundsLabel.width : newSize.width; + newSize.height = Math.max(minBoundsLabel.height, newSize.height); + + // x coordinate gets calculated related to the old position + var deltaX = target.width - newSize.width; + newX = target.x + deltaX / 2; + } + + assign(newBounds, { + x: newX || target.x, + y: target.y, + width: newSize.width, + height: newSize.height + }); + } + else { + newBounds = null; + } -LabelEditingProvider.prototype.update = function(element, newLabel) { this._commandStack.execute('element.updateLabel', { element: element, - newLabel: newLabel + newLabel: newLabel, + newBounds: newBounds }); }; diff --git a/lib/features/label-editing/cmd/UpdateLabelHandler.js b/lib/features/label-editing/cmd/UpdateLabelHandler.js index 7807295b..49e84757 100644 --- a/lib/features/label-editing/cmd/UpdateLabelHandler.js +++ b/lib/features/label-editing/cmd/UpdateLabelHandler.js @@ -1,12 +1,14 @@ 'use strict'; var LabelUtil = require('../LabelUtil'); +var is = require('../../../util/ModelUtil').is; /** * A handler that updates the text of a BPMN element. */ -function UpdateLabelHandler() { +function UpdateLabelHandler(modeling) { + this._modeling = modeling; /** * Set the label and return the changed elements. @@ -37,10 +39,30 @@ function UpdateLabelHandler() { return setText(ctx.element, ctx.oldLabel); } - // API + function postExecute(ctx) { + if (ctx.newBounds){ + // resize textannotation to size of textarea + if(is(ctx.element, 'bpmn:TextAnnotation')){ + return modeling.resizeShape(ctx.element, ctx.newBounds); + } + + // resize external labels + if( ctx.element.label || ctx.element.type === 'label'){ + var target = ctx.element.type === 'label' ? ctx.element : ctx.element.label; + + return modeling.resizeShape(target, ctx.newBounds); + } + + } + } + + // API this.execute = execute; this.revert = revert; + this.postExecute = postExecute; } -module.exports = UpdateLabelHandler; \ No newline at end of file +UpdateLabelHandler.$inject = [ 'modeling' ]; + +module.exports = UpdateLabelHandler; diff --git a/lib/features/modeling/Modeling.js b/lib/features/modeling/Modeling.js index 93d43fc0..b46dbc01 100644 --- a/lib/features/modeling/Modeling.js +++ b/lib/features/modeling/Modeling.js @@ -49,10 +49,11 @@ Modeling.prototype.getHandlers = function() { }; -Modeling.prototype.updateLabel = function(element, newLabel) { +Modeling.prototype.updateLabel = function(element, newLabel, newBounds) { this._commandStack.execute('element.updateLabel', { element: element, - newLabel: newLabel + newLabel: newLabel, + newBounds: newBounds }); }; diff --git a/test/spec/features/label-editing/LabelEditingProviderSpec.js b/test/spec/features/label-editing/LabelEditingProviderSpec.js index b792d921..2ac596b6 100644 --- a/test/spec/features/label-editing/LabelEditingProviderSpec.js +++ b/test/spec/features/label-editing/LabelEditingProviderSpec.js @@ -2,15 +2,24 @@ require('../../../TestHelper'); +var is = require('../../../../lib/util/ModelUtil').is; + /* global bootstrapViewer, inject */ var labelEditingModule = require('../../../../lib/features/label-editing'), coreModule = require('../../../../lib/core'), - draggingModule = require('diagram-js/lib/features/dragging'); + draggingModule = require('diagram-js/lib/features/dragging'), + modelingModule = require('../../../../lib/features/modeling'); + var LabelUtil = require('../../../../lib/features/label-editing/LabelUtil'); +var minBoundsLabel = require('../../../../lib/util/LabelUtil').DEFAULT_LABEL_SIZE, + minBoundsTextbox = { + width: 150, + height: 50 + }; function triggerKeyEvent(element, event, code) { var e = document.createEvent('Events'); @@ -32,13 +41,12 @@ describe('features - label-editing', function() { describe('basics', function() { - var testModules = [ labelEditingModule, coreModule, draggingModule ]; + var testModules = [ labelEditingModule, coreModule, draggingModule, modelingModule ]; beforeEach(bootstrapViewer(diagramXML, { modules: testModules })); it('should register on dblclick', inject(function(elementRegistry, directEditing, eventBus) { - // given var shape = elementRegistry.get('task-nested-embedded'); @@ -51,7 +59,6 @@ describe('features - label-editing', function() { it('should cancel on ', inject(function(elementRegistry, directEditing, eventBus) { - // given var shape = elementRegistry.get('task-nested-embedded'), task = shape.businessObject; @@ -76,7 +83,6 @@ describe('features - label-editing', function() { it('should complete on drag start', inject(function(elementRegistry, directEditing, dragging) { - // given var shape = elementRegistry.get('task-nested-embedded'), task = shape.businessObject; @@ -94,7 +100,6 @@ describe('features - label-editing', function() { it('should submit on root element click', inject(function(elementRegistry, directEditing, canvas, eventBus) { - // given var shape = elementRegistry.get('task-nested-embedded'), task = shape.businessObject; @@ -123,7 +128,7 @@ describe('features - label-editing', function() { describe('details', function() { - var testModules = [ labelEditingModule, coreModule ]; + var testModules = [ labelEditingModule, coreModule, modelingModule ]; beforeEach(bootstrapViewer(diagramXML, { modules: testModules })); @@ -131,13 +136,13 @@ describe('features - label-editing', function() { eventBus, directEditing; - - beforeEach(inject([ 'elementRegistry', 'eventBus', 'directEditing', function(_elementRegistry, _eventBus, _directEditing) { - elementRegistry = _elementRegistry; - eventBus = _eventBus; - directEditing = _directEditing; - }])); - + beforeEach(inject([ 'elementRegistry', 'eventBus', 'directEditing', + function(_elementRegistry, _eventBus, _directEditing) { + elementRegistry = _elementRegistry; + eventBus = _eventBus; + directEditing = _directEditing; + } + ])); function directEditActivate(element) { if (element.waypoints) { @@ -147,59 +152,271 @@ describe('features - label-editing', function() { } } - function directEditUpdate(value) { + function directEditUpdateLabel(value) { directEditing._textbox.textarea.value = value; } - function directEditComplete(value) { - directEditUpdate(value); + function directEditUpdateShape(bounds) { + var textarea = directEditing._textbox.textarea; + if (bounds.x && bounds.y) { + textarea.style.left = bounds.x + 'px'; + textarea.style.top = bounds.y + 'px'; + } + textarea.style.height = bounds.height + 'px'; + textarea.style.width = bounds.width + 'px'; + } + + function directEditComplete(value, bounds) { + if (value) { + directEditUpdateLabel(value); + } + if (bounds) { + directEditUpdateShape(bounds); + } directEditing.complete(); } - function directEditCancel(value) { - directEditUpdate(value); + function directEditCancel(value, bounds) { + if (value) { + directEditUpdateLabel(value); + } + if (bounds) { + directEditUpdateShape(bounds); + } directEditing.cancel(); } describe('command support', function() { - it('should update via command stack', function() { + describe('- label', function() { - // given - var diagramElement = elementRegistry.get('user-task'); + it('should update via command stack', function() { + // given + var diagramElement = elementRegistry.get('user-task'); - var listenerCalled; + var listenerCalled; - eventBus.on('commandStack.changed', function(e) { - listenerCalled = true; + eventBus.on('commandStack.changed', function(e) { + listenerCalled = true; + }); + + // when + directEditActivate(diagramElement); + directEditComplete('BAR', {}); + + // then + expect(listenerCalled).to.be.true; }); - // when - directEditActivate(diagramElement); - directEditComplete('BAR'); - // then - expect(listenerCalled).to.be.true; + it('should be undone via command stack', inject(function(commandStack) { + // given + var diagramElement = elementRegistry.get('user-task'); + + var oldLabel = LabelUtil.getLabel(diagramElement); + + // when + directEditActivate(diagramElement); + directEditComplete('BAR', {}); + + commandStack.undo(); + + // then + var label = LabelUtil.getLabel(diagramElement); + expect(label).to.eql(oldLabel); + })); + }); - it('should undo via command stack', inject(function(commandStack) { + describe('- shape', function() { + it('of TextAnnotation should update via commandStack', function() { + // given + var diagramElement = elementRegistry.get('text-annotation'); + var oldPosition = { x: diagramElement.x, y: diagramElement.y }; + var newBounds = { height: 100, width: 150 }; + + // when + directEditActivate(diagramElement); + + // then expect textarea to be autosizing + expect(directEditing._textbox.textarea.autosizing).to.be.true; + + // when resizing textarea + directEditComplete('', newBounds); + + // then element should have new bounds + expect(diagramElement.x).to.eql(oldPosition.x); + expect(diagramElement.y).to.eql(oldPosition.y); + expect(diagramElement.height).to.eql(newBounds.height); + expect(diagramElement.width).to.eql(newBounds.width); + }); + + + it('of TextAnnotation should be undone via commandStack', inject(function(commandStack) { + // given + var diagramElement = elementRegistry.get('text-annotation'); + var oldBounds = { + x: diagramElement.x, + y: diagramElement.y, + width: diagramElement.width, + height: diagramElement.height + }; + var newBounds = { height: 100, width: 150 }; + + directEditActivate(diagramElement); + directEditComplete('', newBounds); + + // when + commandStack.undo(); + + // then element should have old bounds + expect(diagramElement.x).to.eql(oldBounds.x); + expect(diagramElement.y).to.eql(oldBounds.y); + expect(diagramElement.height).to.eql(oldBounds.height); + expect(diagramElement.width).to.eql(oldBounds.width); + })); + + + describe('of external label should update via commandStack', function() { + + it('to newBounds if newBoundsTextbox > minBoundsTextbox', function() { + // given + var diagramElement = elementRegistry.get('exclusive-gateway').label; + var newBounds = { height: minBoundsTextbox.height + 10, width: minBoundsTextbox.width + 10 }; + + // when + directEditActivate(diagramElement); + + // then expect textarea to be autosizing + expect(directEditing._textbox.textarea.autosizing).to.be.true; + + // when resizing textarea + directEditComplete('', newBounds); + + // then element should have new bounds + expect(diagramElement.height).to.eql(minBoundsTextbox.height + 10); + expect(diagramElement.width).to.eql(minBoundsTextbox.width + 10); + }); + + + it('to minBoundsLabel.width and correct height if newBoundsTextbox <= minBoundsTextbox', function() { + // given + var diagramElement = elementRegistry.get('exclusive-gateway').label; + var newBounds = { height: minBoundsTextbox.height - 10, width: minBoundsTextbox.width - 10 }; + + // when resizing textarea + directEditActivate(diagramElement); + directEditComplete('', newBounds); + + // then element should have minBoundsLabel.width + expect(diagramElement.width).to.eql(minBoundsLabel.width); + + // then element should have max(minBoundsLabel.height, newBounds.height) + expect(diagramElement.height).to.eql(Math.max(minBoundsLabel.height, newBounds.height)); + }); + }); + + + it('of external label should be undone via commandStack', inject(function(commandStack) { + // given + var diagramElement = elementRegistry.get('exclusive-gateway').label; + var oldBounds = { + x: diagramElement.x, + y: diagramElement.y, + width: diagramElement.width, + height: diagramElement.height + }; + var newBounds = { height: 100, width: 200 }; + + directEditActivate(diagramElement); + directEditComplete('', newBounds); + + // when + commandStack.undo(); + + // then element should have old bounds + expect(diagramElement.height).to.eql(oldBounds.height); + expect(diagramElement.width).to.eql(oldBounds.width); + })); + + + it('of element without autosizing textarea should not update', function() { + // given + var diagramElement = elementRegistry.get('user-task'); + var bounds = { + x: diagramElement.x, + y: diagramElement.y, + width: diagramElement.width, + height: diagramElement.height + }; + var newBounds = { width: 120, height: 100 }; + + // when + directEditActivate(diagramElement); + + // then expect textarea to be not autosizing + expect(!!directEditing._textbox.textarea.autosizing).to.be.false; + + // when resizing textarea + directEditComplete('', newBounds); + + // then expect bounds to stay the same + expect(diagramElement.x).to.eql(bounds.x); + expect(diagramElement.y).to.eql(bounds.y); + expect(diagramElement.height).to.eql(bounds.height); + expect(diagramElement.width).to.eql(bounds.width); + }); + + }); + + }); + + + describe('- position', function() { + + it('of external label should stay centered if completing', function() { // given - var diagramElement = elementRegistry.get('user-task'); + var diagramElement = elementRegistry.get('exclusive-gateway').label; + var newBounds = { height: 100, width: 200 }; + var midXOldLabel = diagramElement.x + diagramElement.width / 2; + var oldY = diagramElement.y; - var oldLabel = LabelUtil.getLabel(diagramElement); + // when resizing textarea + directEditActivate(diagramElement); + directEditComplete('', newBounds); + + //then new label should be centered and y remaining the same + var midXNewLabel = diagramElement.x + newBounds.width / 2; + expect(midXNewLabel).to.eql(midXOldLabel); + expect(diagramElement.y).to.eql(oldY); + }); + + + it('of old external label should be centered if undoing', inject(function(commandStack){ + // given + var diagramElement = elementRegistry.get('exclusive-gateway').label; + var oldBounds = { + x: diagramElement.x, + y: diagramElement.y, + width: diagramElement.width, + height: diagramElement.height + }; + var newBounds = { height: 100, width: 200 }; + var midXOldLabel = diagramElement.x + diagramElement.width / 2; + + directEditActivate(diagramElement); + directEditComplete('', newBounds); // when - directEditActivate(diagramElement); - directEditComplete('BAR'); - commandStack.undo(); // then - var label = LabelUtil.getLabel(diagramElement); - expect(label).to.eql(oldLabel); + var midXUndoneLabel = diagramElement.x + diagramElement.width / 2; + expect(midXUndoneLabel).to.eql(midXOldLabel); + expect(diagramElement.y).to.eql(oldBounds.y); })); }); @@ -208,7 +425,6 @@ describe('features - label-editing', function() { describe('should trigger redraw', function() { it('on shape change', function() { - // given var diagramElement = elementRegistry.get('user-task'); @@ -222,7 +438,7 @@ describe('features - label-editing', function() { // when directEditActivate(diagramElement); - directEditComplete('BAR'); + directEditComplete('BAR', {}); // then expect(listenerCalled).to.be.true; @@ -244,7 +460,7 @@ describe('features - label-editing', function() { // when directEditActivate(diagramElement); - directEditComplete('BAR'); + directEditComplete('BAR', {}); // then expect(listenerCalled).to.be.true; @@ -256,14 +472,12 @@ describe('features - label-editing', function() { describe('element support, should edit', function() { function directEdit(elementId) { - return inject(function(elementRegistry, eventBus, directEditing) { var diagramElement = elementRegistry.get(elementId); var label = LabelUtil.getLabel(diagramElement); - // when directEditActivate(diagramElement); @@ -273,19 +487,24 @@ describe('features - label-editing', function() { expect(directEditing.isActive()).to.be.true; - // when - directEditComplete('B'); + // when element has external label or is a textannotation + var LabelOrTextAnnotation = + is(diagramElement, 'bpmn:TextAnnotation') || + !!diagramElement.label || diagramElement.type ==='label'; // then - // expect update to have happened - label = LabelUtil.getLabel(diagramElement); - expect(label).to.equal('B'); + //expect textarea to be autosizing + //else to be not autosizing + expect(!!directEditing._textbox.textarea.autosizing).to.eql(LabelOrTextAnnotation); + // when + directEditComplete('B', {}); // when directEditActivate(diagramElement); directEditCancel('C'); + // expect no label update to have happened label = LabelUtil.getLabel(diagramElement); expect(label).to.equal('B'); @@ -300,7 +519,6 @@ describe('features - label-editing', function() { it('gateway via label', directEdit('exclusive-gateway_label')); - it('event', directEdit('intermediate-throw-event')); it('event via label', directEdit('intermediate-throw-event_label')); @@ -335,12 +553,13 @@ describe('features - label-editing', function() { it('lane without label', directEdit('nested-lane-no-label')); }); + }); describe('sizing', function() { - var testModules = [ labelEditingModule, coreModule ]; + var testModules = [ labelEditingModule, coreModule, modelingModule ]; beforeEach(bootstrapViewer(diagramXML, { modules: testModules, @@ -351,7 +570,7 @@ describe('features - label-editing', function() { describe('textbox should have minimum size', function() { function testTextboxSizing(elementId, zoom, width, height) { - return inject(function(canvas, elementRegistry, directEditing){ + return inject(function(canvas, elementRegistry, directEditing) { // zoom in canvas.zoom(zoom); // grab one element diff --git a/test/spec/features/modeling/UpdateLabelSpec.js b/test/spec/features/modeling/UpdateLabelSpec.js index 05eb3f48..419e2784 100644 --- a/test/spec/features/modeling/UpdateLabelSpec.js +++ b/test/spec/features/modeling/UpdateLabelSpec.js @@ -21,7 +21,7 @@ describe('features/modeling - update label', function() { var startEvent_1 = elementRegistry.get('StartEvent_1'); // when - modeling.updateLabel(startEvent_1, 'bar'); + modeling.updateLabel(startEvent_1, 'bar', null); // then expect(startEvent_1.businessObject.name).to.equal('bar'); @@ -35,7 +35,7 @@ describe('features/modeling - update label', function() { var startEvent_2 = elementRegistry.get('StartEvent_2'); // when - modeling.updateLabel(startEvent_2, 'bar'); + modeling.updateLabel(startEvent_2, 'bar', null); // then expect(startEvent_2.businessObject.name).to.equal('bar'); @@ -49,7 +49,7 @@ describe('features/modeling - update label', function() { var startEvent_1 = elementRegistry.get('StartEvent_1'); // when - modeling.updateLabel(startEvent_1, ''); + modeling.updateLabel(startEvent_1, '', null); // then expect(startEvent_1.businessObject.name).to.equal(''); @@ -64,7 +64,7 @@ describe('features/modeling - update label', function() { var startEvent_1_label = elementRegistry.get('StartEvent_1_label'); // when - modeling.updateLabel(startEvent_1_label, 'bar'); + modeling.updateLabel(startEvent_1_label, 'bar', null); // then expect(startEvent_1.businessObject.name).to.equal('bar'); @@ -72,6 +72,36 @@ describe('features/modeling - update label', function() { })); + it('should resize label when bounds given', inject(function(modeling, elementRegistry) { + // given + var startEvent_1_label = elementRegistry.get('StartEvent_1_label'); + var newBounds = { x: startEvent_1_label.x, y: startEvent_1_label.y, height: 100, width: 150 }; + + // when + modeling.updateLabel(startEvent_1_label, 'bar', newBounds); + + // then + expect(startEvent_1_label.width).to.eql(150); + expect(startEvent_1_label.height).to.eql(100); + })); + + + it('should fail bounds given in incorrect form', inject(function(modeling, elementRegistry) { + + // given + var startEvent_1_label = elementRegistry.get('StartEvent_1_label'); + var newBounds = {}; + + // when + function updateLabel() { + modeling.updateLabel(startEvent_1_label, 'bar', newBounds); + } + + // then + expect(updateLabel).to.throw('newBounds must have {x, y, width, height} properties'); + })); + + it('should change name of task', inject(function(modeling, elementRegistry) { // given @@ -99,7 +129,7 @@ describe('features/modeling - update label', function() { }); // when - modeling.updateLabel(startEvent_1, 'foo'); + modeling.updateLabel(startEvent_1, 'foo', null); // then expect(changedEvent.elements).to.include(startEvent_1); @@ -120,7 +150,7 @@ describe('features/modeling - update label', function() { }); // when - modeling.updateLabel(startEvent_1_label, 'foo'); + modeling.updateLabel(startEvent_1_label, 'foo', null); // then expect(changedEvent.elements).to.include(startEvent_1); diff --git a/test/spec/features/snapping/BpmnSnappingSpec.js b/test/spec/features/snapping/BpmnSnappingSpec.js index 2b17fb60..b0114561 100644 --- a/test/spec/features/snapping/BpmnSnappingSpec.js +++ b/test/spec/features/snapping/BpmnSnappingSpec.js @@ -381,7 +381,8 @@ describe('features/snapping - BpmnSnapping', function() { modelingModule, resizeModule, rulesModule, - snappingModule + snappingModule, + modelingModule ]; beforeEach(bootstrapModeler(diagramXML, { modules: testResizeModules })); @@ -411,7 +412,8 @@ describe('features/snapping - BpmnSnapping', function() { modelingModule, resizeModule, rulesModule, - snappingModule + snappingModule, + modelingModule ]; beforeEach(bootstrapModeler(diagramXML, { modules: testResizeModules }));