feat(BpmnRenderer): align label size/position to text during rendering
Closes #601
This commit is contained in:
parent
4546d39d92
commit
c13ac91e94
|
@ -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 = {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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');
|
||||
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
|
@ -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 bar 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>
|
Loading…
Reference in New Issue