Allows the insertion of shapes on multiple sequence flows

Hey, our custom metamodel allows that multiple elements can have multiple incoming sequence flows. However, when dropping an element on two existing sequence flows, it is only connected to one of them. I will demonstrate this on an example from the demo editor:

Screenshot 2020-06-19 at 00.40.20

Screenshot 2020-06-19 at 00.40.31

Our desired behavior is that the element is connected with both of them (two incoming sequence flows, one outgoing sequence flow). However, only one is selected and connected properly.

The behavior should be located in bpmn-js/lib/features/modeling/behavior/DropOnFlowBehavior.js but I do not have any idea how to change the function properly to take into account all sequence flows:

  this.postExecuted('elements.move', function(context) {

    var shapes = context.shapes,
        targetFlow = context.targetFlow,
        position = context.position;

    if (targetFlow) {
      insertShape(shapes[0], targetFlow, position);
    }
  }, true);

At the moment, targetFlow does only contains this specific single sequence flow.

Thank you in advance for any help or hint.

You could have a look where the targetFlow has been set and checkout whether it’s possible for you to change this behavior.

1 Like

Changing that behavior is difficult because you can only hover one element at a time.

1 Like

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.

1 Like

Interesting solution. Thanks for sharing it! :+1: