Removing elements from Palette and Context Menu in Angular2+ project

Hi,

I am integrating BPMN-js in one of my Angular2+ projects. I am done with most of the requirements and currently, everything is working fine. The only remaining requirement is, I want to remove some of the elements from the Palette as well as from the Context Menu. I have found discussions on these topics here.

It seems like I need to override the default getPaletteEntries and getContextPadEntries by deleting the required entries. I have tried the same, however, nothing seems to work. I am sure that I am doing something wrong or maybe putting the code somewhere at the wrong place.

Following is the code of my bpmn service where I am managing most of the code related to bpmn-js integration.

import {
  ElementRef,
  Injectable,
  Renderer2,
  RendererFactory2,
} from '@angular/core';
import * as BpmnModeler from 'bpmn-js/dist/bpmn-modeler.production.min.js';
import * as BpmnViewer from 'bpmn-js/dist/bpmn-viewer.production.min.js';
import { Subject } from 'rxjs';
import { BpmnConstantsService } from './bpmn-constants.service';
import ContextPadProvider from 'bpmn-js/lib/features/context-pad/ContextPadProvider.js';
import PaletteProvider from 'bpmn-js/lib/features/palette/PaletteProvider.js';

var _contextPadEntries = ContextPadProvider.prototype.getContextPadEntries;
var _paletteEntries = PaletteProvider.prototype.getPaletteEntries;

/* Code not working */
ContextPadProvider.prototype.getContextPadEntries = function (element) {
  const entries = _contextPadEntries.apply(this);
  delete entries['append.end-event'];
  return entries;
};

/* Code not working */
PaletteProvider.prototype.getPaletteEntries = function (element) {
  const entries = _paletteEntries.apply(this);
  delete entries['create.exclusive-gateway'];
  delete entries['create.intermediate-event'];
  delete entries['create.task'];
  delete entries['create.data-store'];
  return entries;
};

@Injectable({
  providedIn: 'root',
})
export class BpmnService {
  private _bpmnModeler: any;
  private _bpmnViewer: any;
  private _renderer: Renderer2 = null;
  private _bpmnInstance: any;

  public eventOutput: Subject<null | string> = new Subject<null>();

  constructor(private _rendererFactory: RendererFactory2) {

    this._bpmnModeler = new BpmnModeler({
      keyboard: { bindTo: document },
    });
    this._bpmnViewer = new BpmnViewer({ keyboard: { bindTo: document } });

    this._renderer = _rendererFactory.createRenderer(null, null);
  }

  public async createNewDiagram(
    xml: string,
    el: ElementRef,
    editableMode: boolean = true
  ): Promise<void> {
    const diagramXML = xml ? xml : BpmnConstantsService.NEW_DIAGRAM_XML;
    await this.openDiagram(diagramXML, el, editableMode);
  }

  public async openDiagram(
    xml: string,
    el: ElementRef,
    editableMode: boolean = true
  ): Promise<void> {
    try {
      if (editableMode) {
        this._bpmnInstance = this._bpmnModeler;
      } else {
        this._bpmnInstance = this._bpmnViewer;
      }
      await this._bpmnInstance.importXML(xml);

      this._bpmnInstance.get('canvas').zoom('fit-viewport', 'auto');

      this._renderer.addClass(el.nativeElement, 'with-diagram');
      this._renderer.removeClass(el.nativeElement, 'with-error');

      var eventBus = this._bpmnInstance.get('eventBus');

      eventBus.on('element.dblclick', (e) => {
        this.eventOutput.next(e.element.id);
      });

    } catch (err) {
      this._renderer.removeClass(el.nativeElement, 'with-diagram');
      this._renderer.addClass(el.nativeElement, 'with-error');
    }
  }

  public attachModelerToCanvas(el: ElementRef): void {
    this._bpmnInstance.attachTo(el.nativeElement);
  }

  public closeDiagram() {
    this._bpmnInstance.destroy();
  }

  public async exportDiagram(): Promise<string> {
    const result = await this._bpmnInstance.saveXML();
    const { xml } = result;

    return xml;
  }

  public zoomController(step: number, resetZoom: boolean = false) {
    if (resetZoom) {
      this._bpmnInstance.get('canvas').zoom('fit-viewport', 'auto');
      return;
    }
    this._bpmnInstance.get('zoomScroll').stepZoom(step);
  }

  public toggleFullScreenView(el: ElementRef) {
    if (!document.fullscreenElement) {
      el.nativeElement.requestFullscreen();
    } else {
      if (document.exitFullscreen) {
        document.exitFullscreen();
      }
    }
  }
}

App component HTML where I am integrating the HTML part,

<div class="content" #jsDropZone>

    <div class="message intro">
        <div class="note">
            Drop BPMN diagram from your desktop or <span (click)="create()">create a new diagram</span> to get
            started.
        </div>
    </div>

    <div class="message error">
        <div class="note">
            <p>Ooops, we could not display the BPMN 2.0 diagram.</p>
        </div>
    </div>

    <div class="canvas" #canvas></div>
    <div class="io-zoom-control">
        <ul class="io-control-list">
            <li><button mat-icon-button (click)="zoomIn()"><mat-icon>zoom_in</mat-icon></button></li>
            <li><button mat-icon-button (click)="zoomOut()"><mat-icon>zoom_out</mat-icon></button></li>
            <li><button mat-icon-button (click)="resetZoom()"><mat-icon>crop_free</mat-icon></button></li>
            <li><button mat-icon-button (click)="fullScreen()">
                <mat-icon *ngIf="!isFullScreenViewActive">zoom_out_map</mat-icon>
                <mat-icon *ngIf="isFullScreenViewActive">zoom_in_map</mat-icon>
            </button></li>
        </ul>
    </div>
</div>

<button (click)="save()">SAVE</button>

App component TS file where I am accessing the service

import { Component, ElementRef, ViewChild } from '@angular/core';
import { MatSnackBar } from '@angular/material/snack-bar';
import { BpmnConstantsService } from './bpmn-constants.service';
import { BpmnService } from './bpmn.service';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css'],
})
export class AppComponent {
  @ViewChild('jsDropZone', { static: true }) private el: ElementRef;
  @ViewChild('canvas', { static: true }) private canvas: ElementRef;

  public isFullScreenViewActive: boolean = false;

  constructor(public bpmnService: BpmnService, private _snackbar: MatSnackBar) {}

  create() {
    this.bpmnService.createNewDiagram(
      BpmnConstantsService.NEW_DIAGRAM_XML,
      this.el
    );
  }

  showSnackBar(message: string) {
    this._snackbar.open(message, 'Ok', { duration: 2000 });
  }

  async ngOnInit() {
    const diagram = localStorage.getItem('diagram');
    await this.bpmnService.createNewDiagram(diagram, this.el, true);

    this.bpmnService?.eventOutput?.subscribe(res => {
      this.showSnackBar(`Clicked element 👉 ${res}`)
    })
  }

  ngAfterContentInit() {
    this.bpmnService.attachModelerToCanvas(this.canvas);
  }

  async save() {
    const xml = await this.bpmnService.exportDiagram();
    localStorage.setItem('diagram', xml);
  }

  zoomIn() {
    this.bpmnService.zoomController(1);
  }

  zoomOut() {
    this.bpmnService.zoomController(-1);
  }

  resetZoom() {
    this.bpmnService.zoomController(0, true)
  }

  fullScreen() {
    this.isFullScreenViewActive = !this.isFullScreenViewActive;
    this.bpmnService.toggleFullScreenView(this.el);
  }
}

Can someone help me with this integration part? Any kind of help is appreciable.

Hi @Ayan_Kumar_Saha, welcome!

The way you are overriding the palette and context pad entries looks very hacky. You should better go the way to define your own providers and override the existing entries.

export default class CustomContextPadProvider {
  constructor(contextPad) {
    contextPad.registerProvider(this);
  }

  getContextPadEntries() {
    return function (entries) {
      delete entries["append.end-event"];
      return entries;
    };
  }
}

CustomContextPadProvider.$inject = ["contextPad"];
export default class CustomPaletteProvider {
  constructor(palette) {
    palette.registerProvider(this);
  }

  getPaletteEntries() {
    return function (entries) {
      delete entries["create.exclusive-gateway"];
      delete entries["create.intermediate-event"];
      delete entries["create.task"];
      delete entries["create.data-store"];
      return entries;
    };
  }
}

CustomPaletteProvider.$inject = ["palette"];

Source: custom palette context pad remove - CodeSandbox

Hi @Niklas_Kiefer,

I have solved the issue for now by hiding the elements through CSS. But I guess that should not be the recommended way.

Can you share an example of an Angular or Typescript-based project? Somehow the solution is not working for typescript/angular.

It shouldn’t matter which JS library/framework you’re using. Are you able to share your full setup (preferably inside a code sandbox) so we can have a detailed look?

Hi @Ayan_Kumar_Saha, you can checkout this project GitHub - sangeeth-repo/ng-bpmn-js: bpmnjs in Angular where I have customized both palette & context menu.

Thanks, @Niklas_Kiefer, and @Sangeeth_VS I took references from both of your codes and have implemented the same. It is working fine. Thanks for the help.

1 Like