How to add ports to the Pool shape from the GoJs Extensions?

I was trying to add ports using makePort from the GoJS examples and show/hide them on mouse enter/leave. It works fine for the left side ports but top, right and bottom ports are hidden by lanes

I tried to create ports outside of the Pool using port.alignmentFocus = spot.opposite() but it does not help, still creates ports inside. Please help.

You could change those “lane” Groups not to be opaque, so that the user could see through them to the ports of the containing Group.

Or you could change the Part.layerName of those lane groups to be “Background”, so that they are always behind the containing group.

Walter, thank you for the answer. I tried to set opacity to 0, it makes Lane header invisible. Setting the Lane layerName to “Background” makes Lane +/- button unreachable.

For the former choice, are you modifying the “Pool” Group template? If so, try setting the Shape.fill to null and remove the Binding on Shape.fill.

Apparently all of the groups have layerName set to “Background” already.

Tried to set shape.fill to null and removed fill binding, Still can not see the top, right and bottom ports as the lanes are covering them. BTW I can not make the lanes transparent as should be able to update their backgrounds

Have the “lane” groups be in the “Background” layer, and have the “pool” groups be in the default layer.

Looks like this now:
It look right when i set Pool placeholder.background = ‘transparent’; but buttons are nor reachable…

You need to make the pools have null brushes for the background and any Shape.fill.

This is what I got:

Here is my code

  1. Pool template
protected getPart(): Group {
    const group: Group = makeGraphObject(Group, 'Auto');
    // group.layerName = 'Background';  // all pools and lanes are always behind all nodes and links
    group.background = new Brush('null');  // can grab anywhere in bounds
    group.movable = true; // allows users to re-order by dragging
    group.copyable = false;  // can't copy lanes or pools
    group.avoidable = false;  // don't impede AvoidsNodes routed Links
    group.locationSpot = Spot.Center;
    group.bind(new Binding('location', 'loc', Point.parse).makeTwoWay(Point.stringify));
    group.computesBoundsIncludingLinks = false;
    // use a simple layout that ignores links to stack the 'lane' Groups on top of each other
    group.layout = makeGraphObject(PoolLayout);
    (group.layout as any).spacing = new Size(0, 0); // no space between lanes
    group.reshapable = true;
    group.bind(new Binding('zOrder'));
    group.bind(new Binding('location'));
    // add shape
    const shape: Shape = makeGraphObject(Shape);
    shape.fill = 'null';
    // shape.bind(new Binding('fill', 'color'));
    group.add(shape);
    const table = makeGraphObject(Panel, 'Table');
    table.defaultColumnSeparatorStroke = 'black';
    const panel = makeGraphObject(Panel, 'Horizontal');
    panel.column = 0;
    panel.angle = 270;
    // add text block
    const textBlock: TextBlock = makeGraphObject(TextBlock);
    textBlock.editable = true;
    textBlock.margin = new Margin(5, 0, 5, 0);
    textBlock.font = 'bold 12pt Helvetica, Arial, sans-serif';
    textBlock.bind(new Binding('text').makeTwoWay());
    panel.add(textBlock);
    table.add(panel);
    const placeholder = makeGraphObject(Placeholder);
    placeholder.background = 'darkgray';
    placeholder.column = 1;
    table.add(placeholder);
    group.add(table);
    // add ports
    group.add(this.makePort('T1', new Spot(0, 0.25), true, true));
    group.add(this.makePort('T2', new Spot(0, 0.5), true, true));
    group.add(this.makePort('T3', new Spot(0, 0.75), true, true));
    group.add(this.makePort('L1', new Spot(0.25, 0), true, true));
    group.add(this.makePort('L2', new Spot(0.5, 0), true, true));
    group.add(this.makePort('L3', new Spot(0.75, 0), true, true));
    group.add(this.makePort('R1', new Spot(0.25, 1), true, true));
    group.add(this.makePort('R2', new Spot(0.5, 1), true, true));
    group.add(this.makePort('R3', new Spot(0.75, 1), true, true));
    group.add(this.makePort('B1', new Spot(1, 0.25), true, true));
    group.add(this.makePort('B2', new Spot(1, 0.5), true, true));
    group.add(this.makePort('B3', new Spot(1, 0.75), true, true));
    group.mouseEnter = ((e, obj) => { this.showPorts(obj.part as Group, true); });
    group.mouseLeave = ((e, obj) => { this.showPorts(obj.part as Group, false); });
    return group;
}
  1. Lane template
protected getPart(): Group {
    const group: Group = makeGraphObject(Group, 'Spot');
    group.name = 'Lane';
    group.layerName = 'Background';
    group.minLocation = new Point(NaN, -Infinity);  // only allow vertical movement
    group.maxLocation = new Point(NaN, Infinity);
    group.bind(new Binding('location', 'loc', Point.parse).makeTwoWay(Point.stringify));
    group.selectionObjectName = 'SHAPE';  // selecting a lane causes the body of the lane to be highlit, not the label
    group.resizable = true;
    // group.opacity = 0;
    group.resizeObjectName = 'SHAPE';  // the custom resizeAdornmentTemplate only permits two kinds of resizing
    const layout = makeGraphObject(LayeredDigraphLayout);
    layout.isInitial = false;  // don't even do initial layout
    layout.isOngoing = false;  // don't invalidate layout when nodes or links are added or removed
    layout.direction = 0;
    layout.columnSpacing = 10;
    layout.layeringOption = LayeredDigraphLayout.LayerLongestPathSource;
    group.layout = layout;
    group.computesBoundsAfterDrag = true;  // needed to prevent recomputing Group.placeholder bounds too soon
    group.computesBoundsIncludingLinks = false;  // to reduce occurrences of links going briefly outside the lane
    group.computesBoundsIncludingLocation = true;  // to support empty space at top-left corner of lane
    group.handlesDragDropForMembers = true;  // don't need to define handlers on member Nodes and Links
    group.mouseDrop = ((e: InputEvent, grp: Group) => { // dropping a copy of some Nodes and Links onto this Group adds them to this Group
      if (!e.diagram.selection.any((n) => {
        return (n instanceof Group && n.category !== 'subprocess') || n.category === 'privateProcess'; })) {
        const ok = grp.addMembers(grp.diagram.selection, true);
        if (ok) {
          PoolHelper.updateCrossLaneLinks(grp);
          PoolHelper.relayoutDiagram(e);
        } else {
          grp.diagram.currentTool.doCancel();
        }
      }
    });
    group.subGraphExpandedChanged = ((grp: Group) => {
      const shp = grp.resizeObject;
      if (grp.diagram.undoManager.isUndoingRedoing) {
        return;
      }
      if (grp.isSubGraphExpanded) {
        shp.height = (grp as any)._savedBreadth;
      } else {
        (grp as any)._savedBreadth = shp.height;
        shp.height = NaN;
      }
      PoolHelper.updateCrossLaneLinks(grp);
    });
    const shape = makeGraphObject(Shape, 'Rectangle');  // this is the resized object
    shape.name = 'SHAPE';
    shape.fill = 'white';
    shape.stroke = null;  // need stroke null here or you gray out some of pool border.
    shape.bind(new Binding('fill', 'color'));
    shape.bind(new Binding('desiredSize', 'size', Size.parse).makeTwoWay(Size.stringify));
    group.add(shape);
    // the lane header consisting of a Shape and a TextBlock
    const panel = makeGraphObject(Panel, 'Horizontal');
    panel.name = 'HEADER';
    panel.angle = 270;  // maybe rotate the header to read sideways going up
    panel.alignment = Spot.LeftCenter;
    panel.alignmentFocus = Spot.LeftCenter;
    group.add(panel);
    const textBox = makeGraphObject(TextBlock);  // the lane label
    textBox.editable = true;
    textBox.margin = new Margin(2, 0, 0, 8);
    textBox.bind( new Binding('visible', 'isSubGraphExpanded').ofObject());
    panel.add(textBox);
    textBox.bind(new Binding('text', 'text').makeTwoWay());
    const button = GraphObject.make('SubGraphExpanderButton');
    button.margin = 4;
    button.angle = -270;  // but this remains always visible!
    panel.add(button);
    const placeholder = makeGraphObject(Placeholder);
    placeholder.padding = 12;
    placeholder.alignment = Spot.TopLeft;
    placeholder.alignmentFocus = Spot.TopLeft;
    group.add(placeholder);
    const collapsedPanel = makeGraphObject(Panel, 'Horizontal');
    collapsedPanel.alignment = Spot.TopLeft;
    collapsedPanel.alignmentFocus = Spot.TopLeft;
    group.add(collapsedPanel);
    const collapsedTextBlock = makeGraphObject(TextBlock);  // this TextBlock is only seen when the swimlane is collapsed
    collapsedTextBlock.name = 'LABEL';
    collapsedTextBlock.editable = true;
    collapsedTextBlock.visible = false;
    collapsedTextBlock.angle = 0;
    collapsedTextBlock.margin = new Margin(6, 0, 0, 20);
    collapsedTextBlock.bind(new Binding('visible', 'isSubGraphExpanded', (e) => !e).ofObject());
    collapsedTextBlock.bind(new Binding('text', 'text').makeTwoWay());
    collapsedPanel.add(collapsedTextBlock);
    group.resizeAdornmentTemplate = this.getResizeAdornmentTemplate();
    return group;
}
// define a custom resize adornment that has two resize handles if the group is expanded
private getResizeAdornmentTemplate(): Adornment {
    const adornment = makeGraphObject(Adornment, 'Spot');
    const placeholder = makeGraphObject(Placeholder);
    adornment.add(placeholder);
    const shapeL = makeGraphObject(Shape); // for changing the length of a lane
    shapeL.desiredSize = new Size(7, 50);
    shapeL.alignment = Spot.Right;
    shapeL.fill = 'lightblue';
    shapeL.stroke = 'dodgerblue';
    shapeL.cursor = 'col-resize';
    shapeL.bind(new Binding('visible', '', (ad) => {
        if (ad.adornedPart === null) {
            return false;
        }
        return ad.adornedPart.isSubGraphExpanded;
    }).ofObject());
    adornment.add(shapeL);
    const shapeW = makeGraphObject(Shape); // for changing the length of a lane
    shapeW.desiredSize = new Size(50, 7);
    shapeW.alignment = Spot.Bottom;
    shapeW.fill = 'lightblue';
    shapeW.stroke = 'dodgerblue';
    shapeW.cursor = 'col-resize';
    shapeW.bind(new Binding('visible', '', (ad) => {
        if (ad.adornedPart === null) {
            return false;
        }
        return ad.adornedPart.isSubGraphExpanded;
    }).ofObject());
    adornment.add(shapeW);
    return adornment;
}

Don’t set GraphObject.background, or set it to null. You are setting it in several places in the “Pool” group.

new Brush('null') returns a Brush showing a CSS color named “null”. That is not the same as no Brush at all. And you should be using go-debug.js until you have debugged everything – you would have gotten a warning about “null” not being a valid CSS color string.

Oh, thank you, I am using go-debug.js now. Found some other issues, thanks a lot. Regarding Pools everything looks fine now just the separator between lanes disappeared. How can I get it back? Now I have:
group.background = null; // can grab anywhere in bounds
shape.fill = null;
placeholder.background = null;

I think the separator you refer to is actually a border around each lane – the main element of an Auto Panel.

Added shape stroke to the lane template and
group.mouseEnter = ((e, obj) => { this.showPorts(obj.part.findTopLevelPart() as Group, true); });
group.mouseLeave = ((e, obj) => { this.showPorts(obj.part.findTopLevelPart() as Group, false); });
to show/hide ports. Looks good so far. Thank you a lot!

Sorry, one more problem… Because of the pool’s null background its label part (first column) reacts on the mouse events only when mouse pointer is on top of the text. Otherwise it gets handled by the parent, which is the canvas in my case. For example if I want to drag the pool having mouse pointer in the first column out of the text box I actually dragging the whole canvas

If the header is a separate Panel, set its background.

If there’s no separate Panel holding the header, stretch the TextBlock, set its TextBlock.textAlign to “center”, and set the TextBlock.background.

Works fine, thank you very much