Thank you for your suggestions. This helped a lot.
Indeed, hovering is a problem. However, in my opinion it is more intuitive to “auto connect” the element to both sequenceFlows anyway.
I finally managed to extend the behavior defined in DropOnFlowBehavior
by a CustomBehavior
that is called afterwards and connects all additional sequenceFlows not yet connected:
import inherits from 'inherits'
import { assign, filter, isNumber } from 'min-dash'
import CommandInterceptor from 'diagram-js/lib/command/CommandInterceptor.js'
import { getMid } from 'diagram-js/lib/layout/LayoutUtil'
import { getApproxIntersection } from 'diagram-js/lib/util/LineIntersection'
export default function CustomBehavior (eventBus, bpmnRules, modeling) {
CommandInterceptor.call(this, eventBus)
// Inserting elements and move them on connection
// targetFlow is only used by DropOnFlowBehavior
this.preExecute('shape.create', function (context) {
const parent = context.parent
const shape = context.shape
const shapeMid = getMid(shape)
const allConnections = []
if (parent.children) {
parent.children.forEach((element) => {
const canInsert = bpmnRules.canInsert(shape, element)
if (canInsert && getApproxIntersection(element.waypoints, shapeMid)) {
allConnections.push(element)
}
})
}
if (allConnections && allConnections.length > 0) {
context.allTargetFlows = allConnections
context.position = shapeMid
// context.parent is already set by DropOnFlowBehavior that is called before
}
}, true)
this.postExecute('shape.create', function (context) {
const shape = context.shape
const selectedTargetFlow = context.targetFlow
const allTargetFlows = context.allTargetFlows
const positionOrBounds = context.position
if (selectedTargetFlow && allTargetFlows && allTargetFlows.length > 0) {
insertShape(shape, selectedTargetFlow, allTargetFlows, positionOrBounds, bpmnRules, modeling)
}
}, true)
// Move element on connection
// Our goal is to connect all previous elements that will not already get connected by the DropOnFlowBehavior
this.preExecute('elements.move', function (context) {
let newParent = context.newParent
const shapes = context.shapes
const delta = context.delta
const shape = shapes[0]
if (!shape || !newParent) {
return
}
// if the new parent is a connection,
// change it to the new parent's parent
if (newParent && newParent.waypoints) {
context.newParent = newParent = newParent.parent
}
const shapeMid = getMid(shape)
const newShapeMid = {
x: shapeMid.x + delta.x,
y: shapeMid.y + delta.y
}
const allConnections = []
if (newParent.children) {
newParent.children.forEach((element) => {
const canInsert = bpmnRules.canInsert(shapes, element)
if (canInsert && getApproxIntersection(element.waypoints, newShapeMid)) {
allConnections.push(element)
}
})
}
if (allConnections && allConnections.length > 0) {
context.allTargetFlows = allConnections
context.position = newShapeMid
}
}, true)
this.postExecuted('elements.move', function (context) {
const shapes = context.shapes
const selectedTargetFlow = context.targetFlow
const allTargetFlows = context.allTargetFlows
const position = context.position
if (selectedTargetFlow && allTargetFlows && allTargetFlows.length > 0) {
insertShape(shapes[0], selectedTargetFlow, allTargetFlows, position, bpmnRules, modeling)
}
}, true)
}
inherits(CustomBehavior, CommandInterceptor)
CustomBehavior.$inject = [
'eventBus',
'bpmnRules',
'modeling'
]
function insertShape (shape, selectedTargetFlow, targetFlow, positionOrBounds, bpmnRules, modeling) {
for (let i = 0; i < targetFlow.length; i++) {
const flow = targetFlow[i]
// selectedTargetFlow is already set by DropOnFlowBehavior
if (flow !== selectedTargetFlow) {
const waypoints = flow.waypoints
let waypointsBefore
let waypointsAfter
let dockingPoint
let incomingConnection
let outgoingConnection
const oldOutgoing = shape.outgoing.slice()
const oldIncoming = shape.incoming.slice()
let mid
if (isNumber(positionOrBounds.width)) {
mid = getMid(positionOrBounds)
} else {
mid = positionOrBounds
}
const intersection = getApproxIntersection(waypoints, mid)
if (intersection) {
waypointsBefore = waypoints.slice(0, intersection.index)
waypointsAfter = waypoints.slice(intersection.index + (intersection.bendpoint ? 1 : 0))
// due to inaccuracy intersection might have been found
if (!waypointsBefore.length || !waypointsAfter.length) {
return
}
dockingPoint = intersection.bendpoint ? waypoints[intersection.index] : mid
// if last waypointBefore is inside shape's bounds, ignore docking point
if (!isPointInsideBBox(shape, waypointsBefore[waypointsBefore.length - 1])) {
waypointsBefore.push(copy(dockingPoint))
}
// if first waypointAfter is inside shape's bounds, ignore docking point
if (!isPointInsideBBox(shape, waypointsAfter[0])) {
waypointsAfter.unshift(copy(dockingPoint))
}
}
const source = flow.source
const target = flow.target
if (bpmnRules.canConnect(source, shape, flow)) {
// reconnect source -> inserted shape
modeling.reconnectEnd(flow, shape, waypointsBefore || mid)
incomingConnection = flow
}
if (bpmnRules.canConnect(shape, target, flow)) {
if (!incomingConnection) {
// reconnect inserted shape -> end
modeling.reconnectStart(flow, shape, waypointsAfter || mid)
}
}
const duplicateConnections = [].concat(
// eslint-disable-next-line no-mixed-operators
incomingConnection && filter(oldIncoming, function (connection) {
return connection.source === incomingConnection.source
// eslint-disable-next-line no-mixed-operators
}) || [],
// eslint-disable-next-line no-mixed-operators
outgoingConnection && filter(oldOutgoing, function (connection) {
return connection.source === outgoingConnection.source
// eslint-disable-next-line no-mixed-operators
}) || []
)
if (duplicateConnections.length) {
modeling.removeElements(duplicateConnections)
}
}
}
}
// helpers /////////////////////
function isPointInsideBBox (bbox, point) {
const x = point.x
const y = point.y
return x >= bbox.x &&
x <= bbox.x + bbox.width &&
y >= bbox.y &&
y <= bbox.y + bbox.height
}
function copy (obj) {
return assign({}, obj)
}
Of course, the code should be improved a lot.