diff --git a/lib/features/snapping/BpmnCreateMoveSnapping.js b/lib/features/snapping/BpmnCreateMoveSnapping.js index 3bbd8f8bc7..c35e42ece4 100644 --- a/lib/features/snapping/BpmnCreateMoveSnapping.js +++ b/lib/features/snapping/BpmnCreateMoveSnapping.js @@ -171,6 +171,33 @@ BpmnCreateMoveSnapping.prototype.addSnapTargetPoints = function(snapPoints, shap snapPoints = this.addSnapTargetPoints(snapPoints, shape, target.parent); } + // When moving a label, FixHoverBehavior changes target to root element. + // If the label's owner (labelTarget) is inside a participant, we need to + // also include shapes from that participant as snap targets. + if (shape.labelTarget) { + var labelOwner = shape.labelTarget; + var participant = getParticipantAncestor(labelOwner); + + if (participant && target !== participant) { + + // Get snap targets from the participant + var participantSnapTargets = this.getSnapTargets(shape, participant); + + // Add mid points for shapes and their labels + forEach(participantSnapTargets, function(snapTarget) { + + // Add the shape's mid point + snapPoints.add('mid', getMid(snapTarget)); + + // Also add the shape's label's mid point if it has one + // (labels are not children of participant, so we need to add them explicitly) + if (snapTarget.label) { + snapPoints.add('mid', getMid(snapTarget.label)); + } + }); + } + } + return snapPoints; }; @@ -287,3 +314,20 @@ function getDockingSnapOrigin(docking, isMove, event) { y: docking.y }; } + +/** + * Get the participant ancestor of an element, if any. + * + * @param {Shape} element + * + * @return {Shape|null} + */ +function getParticipantAncestor(element) { + while (element) { + if (is(element, 'bpmn:Participant')) { + return element; + } + element = element.parent; + } + return null; +} diff --git a/test/spec/features/modeling/LabelBoundsSpec.js b/test/spec/features/modeling/LabelBoundsSpec.js index 9fa26cceb2..bd7618353e 100644 --- a/test/spec/features/modeling/LabelBoundsSpec.js +++ b/test/spec/features/modeling/LabelBoundsSpec.js @@ -151,7 +151,7 @@ describe('label bounds', function() { // then var expectedX = getExpectedX(shape); - expect(shape.label.x).to.equal(expectedX); + expect(shape.label.x).to.be.closeTo(expectedX, DELTA); })); @@ -264,7 +264,7 @@ describe('label bounds', function() { var xml = result.xml; // strip spaces and line breaks after '>' - xml = xml.replace(/>\s+/g,'>'); + xml = xml.replace(/>\s+/g, '>'); // get label width and height from XML var matches = xml.match(/StartEvent_1_di.*?BPMNLabel.*?width="(\d*).*?height="(\d*)/); @@ -308,7 +308,7 @@ describe('label bounds', function() { var xml = result.xml; // strip spaces and line breaks after '>' - xml = xml.replace(/>\s+/g,'>'); + xml = xml.replace(/>\s+/g, '>'); // get label width and height from XML var matches = xml.match(/StartEvent_3_di.*?BPMNLabel.*?width="(\d*).*?height="(\d*)/); diff --git a/test/spec/features/snapping/BpmnCreateMoveSnapping.label-snapping.bpmn b/test/spec/features/snapping/BpmnCreateMoveSnapping.label-snapping.bpmn new file mode 100644 index 0000000000..cfd5f0bc85 --- /dev/null +++ b/test/spec/features/snapping/BpmnCreateMoveSnapping.label-snapping.bpmn @@ -0,0 +1,42 @@ + + + + + + + + + StartEvent_1 + Gateway_1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/spec/features/snapping/BpmnCreateMoveSnappingSpec.js b/test/spec/features/snapping/BpmnCreateMoveSnappingSpec.js index fa9c38fd97..64df25ba16 100644 --- a/test/spec/features/snapping/BpmnCreateMoveSnappingSpec.js +++ b/test/spec/features/snapping/BpmnCreateMoveSnappingSpec.js @@ -485,6 +485,83 @@ describe('features/snapping - BpmnCreateMoveSnapping', function() { }); + describe('label snapping inside participant', function() { + + var diagramXML = require('./BpmnCreateMoveSnapping.label-snapping.bpmn'); + + beforeEach(bootstrapModeler(diagramXML, { + modules: testModules + })); + + + it('should add snap targets from participant when moving label', inject( + function(dragging, elementRegistry, move) { + + // given + var startEvent = elementRegistry.get('StartEvent_1'); + var startEventLabel = startEvent.label; + + var labelMid = mid(startEventLabel); + + // when + move.start(canvasEvent(labelMid), startEventLabel); + + dragging.hover({ element: startEvent.parent, gfx: elementRegistry.getGraphics(startEvent.parent) }); + dragging.move(canvasEventTopLeft({ x: 395, y: labelMid.y + 50 }, startEventLabel)); + dragging.end(); + + // then + expect(mid(startEventLabel)).to.eql({ + x: 400, // 395 snapped to 400 + y: labelMid.y + 50 + }); + } + )); + + }); + + + describe('label snapping in process (no participant)', function() { + + var diagramXML = require('./BpmnCreateMoveSnapping.process.bpmn'); + + beforeEach(bootstrapModeler(diagramXML, { + modules: testModules + })); + + + it('should handle label move when no participant ancestor exists', inject( + function(dragging, elementRegistry, move, modeling, elementFactory, canvas) { + + // given - create a StartEvent with external label in the process (no participant) + var process = elementRegistry.get('Process_1'); + var startEvent = elementFactory.createShape({ type: 'bpmn:StartEvent' }); + + modeling.createShape(startEvent, { x: 300, y: 150 }, process); + modeling.updateLabel(startEvent, 'Start'); + + var startEventLabel = startEvent.label; + + // Verify label was created + expect(startEventLabel).to.exist; + expect(startEventLabel.labelTarget).to.equal(startEvent); + + var labelMid = mid(startEventLabel); + + // when - move the label (getParticipantAncestor returns null for process) + // This exercises the code path where no participant ancestor exists + move.start(canvasEvent(labelMid), startEventLabel); + dragging.move(canvasEvent({ x: labelMid.x + 50, y: labelMid.y })); + dragging.end(); + + // then + expect(startEventLabel.x).to.exist; + } + )); + + }); + + describe('docking points', function() { describe('move mode', function() {