feat(BpmnRenderer): align label size/position to text during rendering

Closes #601
This commit is contained in:
pedesen 2016-09-01 18:06:50 +02:00
parent 4546d39d92
commit c13ac91e94
5 changed files with 403 additions and 7 deletions

View File

@ -305,9 +305,16 @@ function BpmnRenderer(eventBus, styles, pathMap, priority) {
return renderLabel(p, semantic.name, { box: element, align: align, padding: 5 });
}
function renderExternalLabel(p, element, align) {
function renderExternalLabel(p, element) {
var semantic = getSemantic(element);
return renderLabel(p, semantic.name, { box: element, align: align, style: { fontSize: '11px' } });
var box = {
width: 90,
height: 30,
x: element.width / 2 + element.x,
y: element.height / 2 + element.y
};
return renderLabel(p, semantic.name, { box: box, style: { fontSize: '11px' } });
}
function renderLaneLabel(p, text, element) {
@ -1261,7 +1268,27 @@ function BpmnRenderer(eventBus, styles, pathMap, priority) {
});
},
'label': function(p, element) {
return renderExternalLabel(p, element, '');
// Update external label size and bounds during rendering when
// we have the actual rendered bounds anyway.
var textElement = renderExternalLabel(p, element);
var textBBox = textElement.getBBox();
// update element.x so that the layouted text is still
// center alligned (newX = oldMidX - newWidth / 2)
element.x = Math.round(element.x + element.width / 2) - Math.round((textBBox.width / 2));
// take element width, height from actual bounds
element.width = Math.ceil(textBBox.width);
element.height = Math.ceil(textBBox.height);
// compensate bounding box x
textElement.attr({
transform: 'translate(' + (-1 * textBBox.x) + ',0)'
});
return textElement;
},
'bpmn:TextAnnotation': function(p, element) {
var style = {

View File

@ -12,7 +12,6 @@ var getBusinessObject = require('../../util/ModelUtil').getBusinessObject,
var CommandInterceptor = require('diagram-js/lib/command/CommandInterceptor');
/**
* A handler responsible for updating the underlying BPMN 2.0 XML + DI
* once changes on the diagram happen
@ -124,9 +123,33 @@ function BpmnUpdater(eventBus, bpmnFactory, connectionDocking, translate) {
self.updateBounds(shape);
}
this.executed([ 'shape.move', 'shape.create', 'shape.resize' ], ifBpmn(updateBounds));
this.reverted([ 'shape.move', 'shape.create', 'shape.resize' ], ifBpmn(updateBounds));
this.executed([ 'shape.move', 'shape.create', 'shape.resize' ], ifBpmn(function(event) {
// exclude labels because they're handled separately during shape.changed
if (event.context.shape.type === 'label') {
return;
}
updateBounds(event);
}));
this.reverted([ 'shape.move', 'shape.create', 'shape.resize' ], ifBpmn(function(event) {
// exclude labels because they're handled separately during shape.changed
if (event.context.shape.type === 'label') {
return;
}
updateBounds(event);
}));
// Handle labels separately. This is necessary, because the label bounds have to be updated
// every time its shape changes, not only on move, create and resize.
eventBus.on('shape.changed', function(event) {
if (event.element.type === 'label') {
updateBounds({ context: { shape: event.element } });
}
});
// attach / detach connection
function updateConnection(e) {

View File

@ -28,7 +28,7 @@ function triggerKeyEvent(element, event, code) {
return element.dispatchEvent(e);
}
describe('features - label-editing', function() {
describe.only('features - label-editing', function() {
var diagramXML = require('../../../fixtures/bpmn/features/label-editing/labels.bpmn');

View File

@ -0,0 +1,319 @@
'use strict';
/* global bootstrapModeler, inject, sinon */
var Modeler = require('../../../../lib/Modeler');
var TestContainer = require('mocha-test-container-support');
describe('label bounds', function() {
function createModeler(xml, done) {
var modeler = new Modeler({ container: container });
modeler.importXML(xml, function(err, warnings) {
done(err, warnings, modeler);
});
}
var container;
beforeEach(function() {
container = TestContainer.get(this);
});
describe('on import', function() {
it('should import simple label process', function(done) {
var xml = require('./LabelBoundsSpec.simple.bpmn');
createModeler(xml, done);
});
});
describe('on label change', function() {
var diagramXML = require('./LabelBoundsSpec.simple.bpmn');
beforeEach(bootstrapModeler(diagramXML));
var updateLabel;
beforeEach(inject(function(directEditing) {
updateLabel = function(shape, text) {
directEditing.activate(shape);
directEditing._textbox.content.innerText = text;
directEditing.complete();
};
}));
describe('label dimensions', function() {
it('should expand width', inject(function(elementRegistry) {
// given
var shape = elementRegistry.get('StartEvent_1');
// when
updateLabel(shape, 'Foooooooooobar');
// then
// expect the width to be within a range because different browsers...
expect(shape.label.width).to.be.within(82, 84);
}));
it('should expand height', inject(function(elementRegistry) {
// given
var shape = elementRegistry.get('StartEvent_1');
// when
updateLabel(shape, 'Foo\nbar\nbaz');
// then
expect(shape.label.height).to.be.within(36, 45);
}));
it('should reduce width', inject(function(elementRegistry) {
// given
var shape = elementRegistry.get('StartEvent_1');
// when
updateLabel(shape, 'i');
// then
expect(shape.label.width).to.be.within(2, 3);
}));
it('should reduce height', inject(function(elementRegistry) {
// given
var shape = elementRegistry.get('StartEvent_3');
// when
updateLabel(shape, 'One line');
// then
expect(shape.label.height).to.be.within(12, 15);
}));
});
describe('label position', function() {
var getExpectedX = function(shape) {
var shapeMid = shape.x + shape.width/2;
return Math.round(shapeMid - shape.label.width/2);
};
it('should shift to left', inject(function(elementRegistry) {
// given
var shape = elementRegistry.get('StartEvent_1');
// when
updateLabel(shape, 'Foooooooooobar');
// then
var expectedX = getExpectedX(shape);
expect(shape.label.x).to.be.within(expectedX - 1, expectedX);
}));
it('should shift to right', inject(function(elementRegistry) {
// given
var shape = elementRegistry.get('StartEvent_1');
// when
updateLabel(shape, 'F');
// then
var expectedX = getExpectedX(shape);
expect(shape.label.x).to.be.within(expectedX -1, expectedX);
}));
});
describe('label outlines', function() {
it('should update after element bounds have been updated',
inject(function(outline, elementRegistry, bpmnRenderer) {
// given
var shape = elementRegistry.get('StartEvent_1');
var outlineSpy = sinon.spy(outline, 'updateShapeOutline');
var rendererSpy = sinon.spy(bpmnRenderer, 'drawShape');
// when
updateLabel(shape, 'Fooooobar');
// then
// expect the outline updating to happen after the renderer
// updated the elements bounds dimensions and position
sinon.assert.callOrder(
rendererSpy.withArgs(sinon.match.any, shape.label),
outlineSpy.withArgs(sinon.match.any, shape.label)
);
})
);
});
describe('interaction events', function() {
it('should update bounds after element bounds have been updated',
inject(function(interactionEvents, elementRegistry, bpmnRenderer) {
// given
var shape = elementRegistry.get('StartEvent_1'),
gfx = elementRegistry.getGraphics('StartEvent_1_label'),
hit = gfx.select('.djs-hit');
var interactionEventSpy = sinon.spy(hit, 'attr'),
rendererSpy = sinon.spy(bpmnRenderer, 'drawShape');
// when
updateLabel(shape, 'Fooooobar');
// then
// expect the interaction event bounds updating to happen after the renderer
// updated the elements bounds dimensions and position
sinon.assert.callOrder(
rendererSpy.withArgs(sinon.match.any, shape.label),
interactionEventSpy
);
})
);
});
});
describe('on export', function() {
it('should create DI when label has changed', function(done) {
var xml = require('./LabelBoundsSpec.simple.bpmn');
createModeler(xml, function(err, warnings, modeler) {
if (err) {
return done(err);
}
var elementRegistry = modeler.get('elementRegistry'),
directEditing = modeler.get('directEditing');
var shape = elementRegistry.get('StartEvent_1');
directEditing.activate(shape);
directEditing._textbox.content.innerText = 'BARBAZ';
directEditing.complete();
modeler.saveXML({ format: true }, function(err, result) {
if (err) {
return done(err);
}
// strip spaces and line breaks after '>'
result = result.replace(/>\s+/g,'>');
// get label width and height from XML
var matches = result.match(/StartEvent_1_di.*?BPMNLabel.*?width="(\d*).*?height="(\d*)/);
var width = parseInt(matches[1]),
height = parseInt(matches[2]);
expect(width).to.be.within(43, 45);
expect(height).to.be.within(12, 15);
done();
});
});
});
it('should update existing DI when label has changed', function(done) {
var xml = require('./LabelBoundsSpec.simple.bpmn');
createModeler(xml, function(err, warnings, modeler) {
if (err) {
return done(err);
}
var elementRegistry = modeler.get('elementRegistry'),
directEditing = modeler.get('directEditing');
var shape = elementRegistry.get('StartEvent_3');
directEditing.activate(shape);
directEditing._textbox.content.innerText = 'BARBAZ';
directEditing.complete();
modeler.saveXML({ format: true }, function(err, result) {
if (err) {
return done(err);
}
// strip spaces and line breaks after '>'
result = result.replace(/>\s+/g,'>');
// get label width and height from XML
var matches = result.match(/StartEvent_3_di.*?BPMNLabel.*?width="(\d*).*?height="(\d*)/);
var width = parseInt(matches[1]),
height = parseInt(matches[2]);
expect(width).to.be.within(43, 45);
expect(height).to.be.within(12, 15);
done();
});
});
});
it('should not update DI of untouched labels', function(done) {
var xml = require('./LabelBoundsSpec.simple.bpmn');
createModeler(xml, function(err, warnings, modeler) {
if (err) {
return done(err);
}
modeler.saveXML({ format: true }, function(err, result) {
if (err) {
return done(err);
}
expect(result).to.equal(xml);
done();
});
});
});
});
});

View File

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<bpmn:definitions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" id="Definitions_1" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="1.3.0-dev">
<bpmn:process id="Process_1" isExecutable="false">
<bpmn:startEvent id="StartEvent_1" name="foo" />
<bpmn:startEvent id="StartEvent_2" name="bar" />
<bpmn:startEvent id="StartEvent_3" name="foo&#10;bar&#10;baz" />
</bpmn:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_1">
<bpmndi:BPMNShape id="StartEvent_1_di" bpmnElement="StartEvent_1">
<dc:Bounds x="173" y="102" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="StartEvent_2_di" bpmnElement="StartEvent_2">
<dc:Bounds x="293" y="102" width="36" height="36" />
<bpmndi:BPMNLabel>
<dc:Bounds x="303" y="141" width="16" height="12" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="StartEvent_3_di" bpmnElement="StartEvent_3">
<dc:Bounds x="417" y="102" width="36" height="36" />
<bpmndi:BPMNLabel>
<dc:Bounds x="335" y="138" width="200" height="200" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn:definitions>