Push shapes to background and foreground


#1

I implemented curved connections in our modeler and now there are situations when the connections and the shapes are overlapping.
Does pushing the shapes to background/foreground available in the modeler?
If it doesn’t can you help me how to achieve this?
Afaik the z order of the elements is calculated by simply which element is drawn first and last.
So I would be able to rearrange the order by redrawing them but then I realized I should somehow detect which elements are the overlapping ones but I don’t know how to do this.


#2

You can implement a custom ordering with an abstraction of the OrderingProvider in the ordering module in diagram-js.

For reference, checkout the BpmnOrderingProvider.


#3

Thank you, but I need a way to let the user override the order of a shape. First I thought I can update the order.level property via updateProperties but to me it seems this is not possible. Is there any way to update the order from e.g. a PopupProvider?


#4

At the moment, the order can not be set for individual elements. It is determined by the type of element and its parent element. As @tim-kilian said you can achieve this behavior by implementing an ordering provider. You’d also need to be able to remember each elements order.


#5

Okay in this case I would like to display the connections always on top.
I added this provider:

import inherits from 'inherits';

import {
  is
} from 'bpmn-js/lib/util/ModelUtil';

import BpmnOrderingProvider from 'bpmn-js/lib/features/ordering/BpmnOrderingProvider';


export default function CustomOrderingProvider(eventBus, canvas) {

  BpmnOrderingProvider.call(this, eventBus);

  this.getOrdering = function(element, newParent) {

    if (is(element, 'bpmn:SequenceFlow')) {

      // always move to end of root element
      // to display always on top
      return {
        parent: canvas.getRootElement(),
        index: -1
      };
    }
  };
}

CustomOrderingProvider.$inject = [ 'eventBus', 'canvas' ];

inherits(CustomOrderingProvider, BpmnOrderingProvider);

Which is working fine until the point I am placing a shape on top of my connection, see this anim gif:
chrome-capture

How can I strictly force my connection to be on top no matter what? Okay the labels can be on top of connections but nothing else.


#6

We actually implemented the same behavior for labels to make sure they are always on top. As you can see this doesn’t work either:

label-on-top-compressed

You’d have to change the way getOrdering is implemented in order to achieve the desired behavior.


#7

Well it turned out it would be okay for the connections to stay on top only until someone is placing another shape on top of them.
So I just wanted to apply this to only our curved connections and not the default sequenceFlows.
It turned out OrderingProvider is only handling connection.create and connection.move which is not enough in my case. So I decided to not use OrderingProvider and BpmnOrderingProvider but melt them into a CustomOrderingProvider that you can see here:

import inherits from 'inherits';

import {
  isAny
} from 'bpmn-js/lib/features/modeling/util/ModelingUtil';

import { getBusinessObject, is } from 'bpmn-js/lib/util/ModelUtil';

import {
  findIndex,
  find
} from 'min-dash';

import CommandInterceptor from 'diagram-js/lib/command/CommandInterceptor';

export default function CustomOrderingProvider(eventBus, canvas, translate) {

  CommandInterceptor.call(this, eventBus);

  this.preExecute([ 'shape.create', 'connection.create' ], function(event) {

    var context = event.context,
        element = context.shape || context.connection,
        parent = context.parent;

    var ordering = getOrdering(element, parent);

    if (ordering) {

      if (ordering.parent !== undefined) {
        context.parent = ordering.parent;
      }

      context.parentIndex = ordering.index;
    }
  });

  this.preExecute([ 'shape.move', 'connection.move', 'connection.reconnectStart', 'connection.reconnectEnd', 'connection.updateWaypoints', 'connection.layout' ], function(event) {

    var context = event.context,
        element = context.shape || context.connection,
        parent = context.newParent || element.parent;

    var ordering = getOrdering(element, parent);

    if (ordering) {

      if (ordering.parent !== undefined) {
        context.newParent = ordering.parent;
      }

      context.newParentIndex = ordering.index;
    }
  });


  var orders = [
    { type: 'bpmn:SubProcess', order: { level: 6 } },
    {
      type: 'bpmn:SequenceFlow',
      order: {
        level: 3,
        containers: [
          'bpmn:Participant',
          'bpmn:FlowElementsContainer'
        ]
      }
    },
    // handle DataAssociation(s) like message flows and render them always on top
    {
      type: 'bpmn:DataAssociation',
      order: {
        level: 9,
        containers: [
          'bpmn:Collaboration',
          'bpmn:Process'
        ]
      }
    },
    {
      type: 'bpmn:MessageFlow', order: {
        level: 9,
        containers: [ 'bpmn:Collaboration' ]
      }
    },
    {
      type: 'bpmn:Association',
      order: {
        level: 6,
        containers: [
          'bpmn:Participant',
          'bpmn:FlowElementsContainer',
          'bpmn:Collaboration'
        ]
      }
    },
    { type: 'bpmn:BoundaryEvent', order: { level: 8 } },
    { type: 'bpmn:FlowElement', order: { level: 5 } },
    { type: 'bpmn:Participant', order: { level: -2 } },
    { type: 'bpmn:Lane', order: { level: -1 } }
  ];

  function computeOrder(element) {
    if (element.labelTarget) {
      return { level: 10 };
    }

    var entry = find(orders, function(o) {
      return isAny(element, [ o.type ]);
    });

    return entry && entry.order || { level: 1 };
  }

  function getOrder(element) {

    var order = element.order;

    if (!order) {
      element.order = order = computeOrder(element);
    }

    return order;
  }

  function findActualParent(element, newParent, containers) {

    var actualParent = newParent;

    while (actualParent) {

      if (isAny(actualParent, containers)) {
        break;
      }

      actualParent = actualParent.parent;
    }

    if (!actualParent) {
      throw new Error(translate('no parent for {element} in {parent}', {
        element: element.id,
        parent: newParent.id
      }));
    }

    return actualParent;
  }

  function getOrdering(element, newParent) {

    // render labels always on top
    if (element.labelTarget) {
      return {
        parent: canvas.getRootElement(),
        index: -1
      };
    }

    var elementOrder = getOrder(element);


    if (elementOrder.containers) {
      newParent = findActualParent(element, newParent, elementOrder.containers);
    }

    var bo = getBusinessObject(element);
    var lineType = bo.di.get('line-type');
    if (is(element, 'bpmn:SequenceFlow') && lineType && (lineType === 'curved' || lineType === 'curved-thick')) {

      // always move to end of root element
      // to display always on top
      return {
        parent: newParent,
        index: -1
      };
    }

    var currentIndex = newParent.children.indexOf(element);

    var insertIndex = findIndex(newParent.children, function(child) {

      // do not compare with labels, they are created
      // in the wrong order (right after elements) during import and
      // mess up the positioning.
      if (!element.labelTarget && child.labelTarget) {
        return false;
      }

      return elementOrder.level < getOrder(child).level;
    });


    // if the element is already in the child list at
    // a smaller index, we need to adjust the inser index.
    // this takes into account that the element is being removed
    // before being re-inserted
    if (insertIndex !== -1) {
      if (currentIndex !== -1 && currentIndex < insertIndex) {
        insertIndex -= 1;
      }
    }

    return {
      index: insertIndex,
      parent: newParent
    };
  }
}

CustomOrderingProvider.$inject = [ 'eventBus', 'canvas', 'translate' ];

inherits(CustomOrderingProvider, CommandInterceptor);

So as you can see I didn’t change much, but added connection.reconnectStart connection.reconnectEnd connection.updateWaypoints connection.layout hooks into the list.
The other modification is this addition:

var bo = getBusinessObject(element);
    var lineType = bo.di.get('line-type');
    if (is(element, 'bpmn:SequenceFlow') && lineType && (lineType === 'curved')) {

      // always move to end of root element
      // to display always on top
      return {
        parent: newParent,
        index: -1
      };
    }

Which is checking if my connection is a curved one and if yes it should bring to the front. But unfortunately it doesn’t!
My problem is that I had almost the same code as I post 3 days ago and that worked for normal sequenceFlows.
Can you help me in this please?


#8

Have you tried to debug it in order to find out what the problem is?


#9

Sure, but yeah, I should try again because I was not so thorough.


#10

I noticed the curved shape gets on top if I put it inside a participant. See the anim gif:

chrome-capture

I also think there is something else somewhere that is influencing the sequenceFlows because I tried to replace my CustomOrderingProvider with the one that you provide in CustomOrderingProvider.js with the exception that I don’t use custom:connection but bpmn:sequenceFlow but the result is still not okay in case of sequenceFlows.