feat(modeling): prevent accidential dragging of container elements

This implements custom hit areas for participants, lanes and
expanded subprocesses.

Given these changes, users need to grab container elements
on the boarder or the label area to move them.

Closes https://github.com/bpmn-io/bpmn-js/issues/957
This commit is contained in:
Philipp Fromme 2019-06-14 15:29:00 +02:00 committed by Nico Rehwaldt
parent 1a6b6dc46a
commit ab56fc21ad
4 changed files with 264 additions and 0 deletions

View File

@ -24,6 +24,7 @@ import CreateModule from 'diagram-js/lib/features/create';
import DistributeElementsModule from './features/distribute-elements';
import EditorActionsModule from './features/editor-actions';
import GridSnappingModule from './features/grid-snapping';
import InteractionEventsModule from './features/interaction-events';
import KeyboardModule from './features/keyboard';
import KeyboardMoveSelectionModule from 'diagram-js/lib/features/keyboard-move-selection';
import LabelEditingModule from './features/label-editing';
@ -218,6 +219,7 @@ Modeler.prototype._modelingModules = [
DistributeElementsModule,
EditorActionsModule,
GridSnappingModule,
InteractionEventsModule,
KeyboardModule,
KeyboardMoveSelectionModule,
LabelEditingModule,

View File

@ -0,0 +1,106 @@
import { is } from '../../util/ModelUtil';
import { isExpanded } from '../../util/DiUtil';
var LABEL_WIDTH = 30,
LABEL_HEIGHT = 30;
/**
* BPMN-specific hit zones and interaction fixes.
*
* @param {EventBus} eventBus
* @param {InteractionEvents} interactionEvents
*/
export default function BpmnInteractionEvents(eventBus, interactionEvents) {
this._interactionEvents = interactionEvents;
var self = this;
eventBus.on([
'interactionEvents.createHit',
'interactionEvents.updateHit'
], function(context) {
var element = context.element,
gfx = context.gfx;
if (is(element, 'bpmn:Lane')) {
return self.createParticipantHit(element, gfx);
} else
if (is(element, 'bpmn:Participant')) {
if (isExpanded(element)) {
return self.createParticipantHit(element, gfx);
} else {
return self.createDefaultHit(element, gfx);
}
} else
if (is(element, 'bpmn:SubProcess')) {
if (isExpanded(element)) {
return self.createSubProcessHit(element, gfx);
} else {
return self.createDefaultHit(element, gfx);
}
}
});
}
BpmnInteractionEvents.$inject = [
'eventBus',
'interactionEvents'
];
BpmnInteractionEvents.prototype.createDefaultHit = function(element, gfx) {
this._interactionEvents.removeHits(gfx);
this._interactionEvents.createDefaultHit(element, gfx);
// indicate that we created a hit
return true;
};
BpmnInteractionEvents.prototype.createParticipantHit = function(element, gfx) {
// remove existing hits
this._interactionEvents.removeHits(gfx);
// add outline hit
this._interactionEvents.createBoxHit(gfx, 'click-stroke', {
width: element.width,
height: element.height
});
// add label hit
this._interactionEvents.createBoxHit(gfx, 'all', {
width: LABEL_WIDTH,
height: element.height
});
// indicate that we created a hit
return true;
};
BpmnInteractionEvents.prototype.createSubProcessHit = function(element, gfx) {
// remove existing hits
this._interactionEvents.removeHits(gfx);
// add outline hit
this._interactionEvents.createBoxHit(gfx, 'click-stroke', {
width: element.width,
height: element.height
});
// add label hit
this._interactionEvents.createBoxHit(gfx, 'all', {
width: element.width,
height: LABEL_HEIGHT
});
// indicate that we created a hit
return true;
};

View File

@ -0,0 +1,6 @@
import BpmnInteractionEvents from './BpmnInteractionEvents';
export default {
__init__: [ 'bpmnInteractionEvents' ],
bpmnInteractionEvents: [ 'type', BpmnInteractionEvents ]
};

View File

@ -0,0 +1,150 @@
import {
queryAll as domQueryAll
} from 'min-dom';
import {
getBpmnJS,
bootstrapModeler,
inject
} from 'test/TestHelper';
import modelingModule from 'lib/features/modeling';
import coreModule from 'lib/core';
import interactionEventsModule from 'lib/features/interaction-events';
import createModule from 'diagram-js/lib/features/create';
import moveModule from 'diagram-js/lib/features/move';
var testModules = [
coreModule,
modelingModule,
interactionEventsModule,
createModule,
moveModule
];
var HIT_ALL_CLS = 'djs-hit-all';
var HIT_CLICK_STROKE_CLS = 'djs-hit-click-stroke';
describe('features/interaction-events', function() {
describe('participant hits', function() {
var diagramXML = require('test/fixtures/bpmn/collaboration.bpmn');
beforeEach(bootstrapModeler(diagramXML, {
modules: testModules
}));
beforeEach(inject(function(dragging) {
dragging.setOptions({ manual: true });
}));
it('should create two hit zones per participant', inject(function(elementRegistry) {
// given
var participant = elementRegistry.get('Participant_1');
// then
expectToHaveChildren(HIT_ALL_CLS, 1, participant);
expectToHaveChildren(HIT_CLICK_STROKE_CLS, 1, participant);
}));
it('should create two hit zones per lane', inject(function(elementRegistry) {
// given
var lane = elementRegistry.get('Lane_1');
// then
expectToHaveChildren(HIT_ALL_CLS, 1, lane);
expectToHaveChildren(HIT_CLICK_STROKE_CLS, 1, lane);
}));
it('should create one hit zone per collapsed participant',
inject(function(elementRegistry, bpmnReplace) {
// given
var participant = elementRegistry.get('Participant_1');
// when
var collapsedParticipant = bpmnReplace.replaceElement(participant, {
type: 'bpmn:Participant',
isExpanded: false
});
// then
expectToHaveChildren(HIT_ALL_CLS, 1, collapsedParticipant);
})
);
});
describe('sub process hits', function() {
var diagramXML = require('test/fixtures/bpmn/containers.bpmn');
beforeEach(bootstrapModeler(diagramXML, {
modules: testModules
}));
beforeEach(inject(function(dragging) {
dragging.setOptions({ manual: true });
}));
it('should create two hit zones per sub process', inject(function(elementRegistry) {
// given
var subProcess = elementRegistry.get('SubProcess_1');
// then
expectToHaveChildren(HIT_ALL_CLS, 1, subProcess);
expectToHaveChildren(HIT_CLICK_STROKE_CLS, 1, subProcess);
}));
it('should create one hit zone per collapsed sub process',
inject(function(elementRegistry, bpmnReplace) {
// given
var subProcess = elementRegistry.get('SubProcess_1');
// when
var collapsedSubProcess = bpmnReplace.replaceElement(subProcess, {
type: 'bpmn:SubProcess',
isExpanded: false
});
// then
expectToHaveChildren(HIT_ALL_CLS, 1, collapsedSubProcess);
expectToHaveChildren(HIT_CLICK_STROKE_CLS, 0, collapsedSubProcess);
})
);
});
});
// helper ///////////
function expectToHaveChildren(className, expectedCount, element) {
var selector = '.' + className;
var elementRegistry = getBpmnJS().get('elementRegistry'),
gfx = elementRegistry.getGraphics(element),
realCount = domQueryAll(selector, gfx).length;
expect(
realCount,
'expected ' + element.id + ' to have ' + expectedCount +
' children mat ' + selector + ' but got ' + realCount
).to.eql(expectedCount);
}