Browser default contextmenu also appears on contextclicked event

I’m trying to prevent the default contextmenu of the browser when right clicking on the diagram. Otherwise, the context template I implemented would show up along with the browser default contextmenu when I right-click the mouse.

Interestingly, the browser default contextmenu does only show up along when I right click in certain environments. I’m going to list-up the certain environments:

Windows

  • Whale(2.6.88.13) // latest
  • Chrome(78.0.x) // latest
  • Firefox(70.0.1) // latest

Mac

  • Whale(2.6.88.11) // latest

In other words, when I run my app in chrome, Mac os, the browser default contextmenu doesn’t show up along. I don’t know what’s going on here :/

I’ve even tried like:

this.diagram.addDiagramListener('BackgroundContextClicked', (event: go.DiagramEvent) => {
  event.diagram.lastInput.event.preventDefault();

but it didn’t work.

Normally, the context click event handling and showing of context menus is handled by the ContextMenuTool. I believe that we prevent the browser default context menu from showing because we call https://developer.mozilla.org/en-US/docs/Web/API/Event/preventDefault.

I’m not familiar with Whale. However, on Windows, in https://gojs.net/latest/samples/basic.html, I never see the browser default context menu when using Internet Explorer 11, Edge, Firefox, or Chrome.

So I cannot reproduce the problem that you are reporting. Have you modified the behavior of your Diagram in any ways, such as by disabling the ContextMenuTool or overriding methods on it or in the ToolManager?

That’s weird because I’ve taken a look but I never touched ContextMenuTool in my code and what I only did with ToolManager is just about drag thing. And as I mentioned above, it only happens on those certain environments.

When you try context menus in https://gojs.net/latest/samples/basic.html, do you ever see the browser’s context menu? I’m wondering if there is some external code in your app that is interfering with mouse events.

Wow I don’t see the browser’s context menu on that page :o
I’ve looked into my code and I’m positive there’s no related code about it.

If you do not have a problem with any of our sample apps in any browser, it must be something in your app. Because you have checked that it is not in your code, that is why I believe the problem is in the framework or in the other libraries that you are using.

If I log the event type on BackgroundContextClicked event as:

this.diagram.addDiagramListener('BackgroundContextClicked', (event: go.DiagramEvent) => {
  console.log(event.diagram.lastInput.event.type); // "mouseup"
});

And that was the issue in my case because as you said, your library internally prevents the default contextmenu from showing up on contextmenu event but interestingly, there is a difference in the contextmenu event occurrence order in each browser. For example, in chrome, Mac os, the contextmenu event occurs in mousedown -> contextmenu -> mouseup so your internal prevention for contextmenu works but in other case such as in chrome, Windows, the same event occurs in mousedown -> mouseup -> contextmenu, which means the BackgroundContextClicked event occurs before the contextmenu event and since I open a modal on that event on that point(coordinate), the contextmenu event occurs on the modal, not on the canvas you prevents. That was the issue.

Testing on macOS Mojave (10.14.5) with Safari 12.1.1, I don’t see anything like this.

Does this only happen in Whale? Or does it also happen in Safari? This might be a bug in Whale, but we’ll try to accommodate it if possible.

It’s not a browser bug. It just results from the event occurrence order difference in browsers. Chrome, Firefox and Safari in macOS have the same order: mousedown -> contextmenu -> mouseup and the rest(Chrome, Firefox in Windows) have the same order: mousedown -> mouseup -> contextmenu. I think the issue is that the event you implements(BackgroundContextClicked) occurs on mouseup, not on contextmenu. And that could cause different actions because of the above event order difference.

Yes, but why do you never see the browser’s context menu in any browser on any platform when you run our samples? Or is it the HTML that you are showing in the “BackgroundContextClicked” DiagramEvent that is permitting the browser’s context menu to be shown?

I’m not sure about your samples. It seems that the way it binds the contextmenu event is different between your sample and my code so it might have caused the difference in behavior. And yeah I didn’t prevent the contextmenu event from occurring in the modal I display through the BackgroundContextClicked because I felt it’s unnecessary if there’s an assumption the contextmenu event will always be occurring on the canvas element.

I was just guessing about your HTML. What you say about that makes sense.

If you can tell us how to reproduce the problem, we can investigate it further.

Hello,

I’m having the same problem that was reported here by @QooQooDaas. Is there any solution by now?

I had a look at the example Basic GoJS Sample you mentioned @walter and I saw that the example uses the contextMenu event while this topic is about the contextClick event (at least according to the title). I also use the contextClick event because I’m building a component and I want to propagate an event to the application. Then the application will utilize the information to show a context menu accordingly. So it might be that you don’t see the bug because of the different event type.

Here is part of my code:

   nodeTemplateMap.add(
      'my_gojs_obj_node',
      g(
        go.Node,
        'Auto',
        {
          isShadowed: false,
          selectionAdorned: false,
          mouseEnter: ndMouseEnter,
          mouseLeave: ndMouseLeave,
          selectionChanged: nodeSelectionChanged,
          contextClick: nodeContextClickHandler,
        },
        g(
          go.Shape,
          { name: 'anode', figure: 'Ellipse', strokeWidth: 8 },
          new go.Binding('stroke', '', selectColor2),
          new go.Binding('fill', 'obj_type', selectColor)
        ),
        ...
function nodeContextClickHandler(e, obj) {
    const itemList = [];
    itemList.push({
      type: obj.part.data.ctx_type,
      id: obj.part.data.ctx_id,
    });
    const data = {
      itemList: itemList,
      position: {
        x: e.diagram.lastInput.event.clientX,
        y: e.diagram.lastInput.event.clientY
      },
    };  
    self.report.myService.openContextMenu(data);
  }

I’m running the code on Windows 10 Enterprise 64bit and I have tested with:
Chrome Version 91.0.4472.106
Firefox 78.7.1esr
Edge Version 91.0.864.54
In all cases I see the browser default context menu on top of my context menu.

I have tried several things to prevent this like:
e.handled = true;
e.event.defaultPrevented = true;
e.preventDefault();
e.stopPropagation();
But none of them worked.

Also, based on the observation from @QooQooDaas I tried to change the event type with:
e.down = true;
e.up = false;
But again it didn’t do the trick.

The weirdest thing is that the same code worked just fine in our previous AngularJS application. Now that the code is migrated to Angular 11 we experience this problem. I’m not sure if the Angular framework affects this somehow or it is related to something else (because we also use different GoJS version now).

Thank you in advance for you help!

I tried to use your code, but it was fruitless because I have no idea of what openContextMenu does.

I suppose you could use the older version of GoJS to see if that matters. My guess is that it won’t.

Are you using gojs-angular? The 2.0 release of gojs-angular is incompatible – it assumes that the data external to GoJS is immutable.

Hi @walter, thanks for your answer.

I checked again now and I saw that we actually use the same version in both the old and the new code. It is gojs@1.8.38. Sorry for the wrong info in my previous post.
Is there any example where you use the contextClick for creating a menu?

PS: The openContextMenu simply emits the data as an event (output parameter) and then the application listens to this parameter and it creates the context menu accordingly.

If you are using Angular, we really recommend upgrading to version 2.1.

What does your openContextMenu function do to show a context menu? Is the context menu implemented in HTML? If so, GoJS really isn’t involved with its appearance or behavior.

I think the problem here is that the event keeps bubbling up from the one element to the other and it goes up to the browser in the end.

The same code that I posted already in the nodeContextClickHandler is also used for context menus in tables (where there are no charts). There I have the same behaviour (i.e. the browser menu is shown on top of the context menu) but this problem is fixed once I add the following lines after the openContextMenu:

event.preventDefault();
event.stopPropagation();

But when I add the same 2 lines after the openContextMenu in the case of the GoJS charts, the problem is not fixed. This means that the problem doesn’t come from the code that the openContextMenu calls, because, if this was the case, we would have the same persistent problem also in the tables. But the problem happens only in the charts.

I saw here GoJS Events -- Northwoods Software that it says:

The event handler is held as the value of a property on the object. It then also “bubbles” the event up the chain of GraphObject.panels until it ends with a Part. This allows a “click” event handler to be declared on a Panel and have it apply even if the click actually happens on an element deep inside the panel. If there is no object at the mouse point, the event occurs on the diagram.

So my question is: Can you please add the preventDefault() and the stopPropagation() in your code when the event reaches the last (mother) element so that the event doesn’t go further to the browser?

It seems that this is something that I cannot control in my code somehow and this is really sad.

I believe if you experiment with a simple example of a contextClick you will see the problem yourself.

I still don’t have enough information for me to understand exactly what you are doing. Your code is also confusing DOM events with GoJS InputEvents. There are no methods named preventDefault or stopPropagation on the InputEvent class.

But you can set InputEvent.handled to true and InputEvent.bubbles to false. The latter will cause Event.stopPropagation to be called and (if Event.cancelable is true) Event.preventDefault to be called.

It still doesn’t make sense to me why you didn’t have a problem before but you do now after upgrading to Angular 11. What’s going on with that change?

So I’m guessing you are creating the context menu in HTML. Where is it in the DOM relative to the Diagram.div? It should not be inside that HTMLDivElement.

For an example of a custom HTML context menu implemented using the standard GoJS mechanisms (HTMLInfo): HTML Context Menu

Hi @walter,

As I wrote already in my first post, I have already experimented with the e.handled=true and this didn’t work. Now I added also e.bubbles=false as you suggested but still it doesn’t fix the problem. (BTW the API documentation InputEvent | GoJS API doesn’t contain this property and thus I didn’t know about it.)

Actually I noticed that the event properties are not set according to the properties I have set. The event.bubbles is still true and the event.defaultPrevented is still false (see attached screenshot).

To your question about the DOM, no the context menu is completely out of the whole canvas. It is a mat-dialog-container which is placed on top of the whole canvas (with z-index property and according to the X and Y values that are passed via the event).

Your example GoJS/customContextMenu.html at master · NorthwoodsSoftware/GoJS · GitHub again uses the contextMenu event (like the other example you wrote in your first post in this thread). Can you please create an example using the contextClick event? I think then you will see the problem.

OK, here’s a complete sample that shows an HTML context menu without making use of the ContextMenuTool nor the GraphObject.contextMenu or Diagram.contextMenu properties.

There’s no call to stopPropagation anywhere, but there is a “contextmenu” DOM event listener that calls preventDefault. There’s no setting of any InputEvent property, either.

<!DOCTYPE html>
<html>
<head>
  <title>Minimal GoJS Sample</title>
  <!-- Copyright 1998-2021 by Northwoods Software Corporation. -->
  <meta name="description" content="">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <script src="https://unpkg.com/gojs"></script>

  <style type="text/css">
    /* CSS for the traditional context menu, copied from https://gojs.net/latest/samples/customContextMenu.html */
    .menu {
      display: none;
      position: absolute;
      opacity: 0;
      margin: 0;
      padding: 8px 0;
      z-index: 999;
      box-shadow: 0 5px 5px -3px rgba(0, 0, 0, .2), 0 8px 10px 1px rgba(0, 0, 0, .14), 0 3px 14px 2px rgba(0, 0, 0, .12);
      list-style: none;
      background-color: #ffffff;
      border-radius: 4px;
    }

    .menu-item {
      display: block;
      position: relative;
      min-width: 60px;
      margin: 0;
      padding: 6px 16px;
      font: bold 12px sans-serif;
      color: rgba(0, 0, 0, .87);
      cursor: pointer;
    }

    .menu-item::before {
      position: absolute;
      top: 0;
      left: 0;
      opacity: 0;
      pointer-events: none;
      content: "";
      width: 100%;
      height: 100%;
      background-color: #000000;
    }

    .menu-item:hover::before {
      opacity: .04;
    }

    .menu .menu {
      top: -8px;
      left: 100%;
    }

    .show-menu, .menu-item:hover > .menu {
      display: block;
      opacity: 1;
    }
  </style>

  <script id="code">
  function init() {
    var $ = go.GraphObject.make;

    myDiagram =
      $(go.Diagram, "myDiagramDiv",
        {
          contextClick: function(e) { showContextMenu(null, e.diagram); },
          "undoManager.isEnabled": true
        });


    // This is the actual HTML context menu.
    // Copied from https://gojs.net/latest/samples/customContextMenu.html
    var cxElement = document.getElementById("contextMenu");

    // We don't want the div acting as a context menu to have a (browser) context menu!
    cxElement.addEventListener("contextmenu", function(e) {
      e.preventDefault();
      return false;
    }, false);


   myDiagram.nodeTemplate =
      $(go.Node, "Auto",
        {
          contextClick: function(e, node) { showContextMenu(node, e.diagram); }
        },
        new go.Binding("location", "loc").makeTwoWay(),
        $(go.Shape, { fill: "white" }),
        $(go.TextBlock,
          { margin: 8 },
          new go.Binding("text", "key"))
      );

    myDiagram.model =
      $(go.GraphLinksModel, {
        linkKeyProperty: "k",
        nodeDataArray: [
		{ key: 1 },
		{ key: 2 },
		{ key: 3 },
		{ key: 4 }
	],
        linkDataArray: [
		{ from: 1, to: 2 },
		{ from: 1, to: 3 },
		{ from: 3, to: 4 }
	]});
  }


// Copied from https://gojs.net/latest/samples/customContextMenu.html
// and adapted not to use the ContextMenuTool.

  function showContextMenu(obj, diagram) {
    // This is the actual HTML context menu:
    var cxElement = document.getElementById("contextMenu");

    // Show only the relevant buttons given the current state.
    var cmd = diagram.commandHandler;
    var hasMenuItem = false;

    function maybeShowItem(elt, pred) {
	if (pred) {
		elt.style.display = "block";
		hasMenuItem = true;
	} else {
		elt.style.display = "none";
	}
    }

	maybeShowItem(document.getElementById("cut"), cmd.canCutSelection());
	maybeShowItem(document.getElementById("copy"), cmd.canCopySelection());
	maybeShowItem(document.getElementById("paste"), cmd.canPasteSelection(diagram.toolManager.contextMenuTool.mouseDownPoint));
	maybeShowItem(document.getElementById("delete"), cmd.canDeleteSelection());

	// Now show the whole context menu element
	if (hasMenuItem) {
		cxElement.classList.add("show-menu");
		// we don't bother overriding positionContextMenu, we just do it here:
		var mousePt = diagram.lastInput.viewPoint;
		cxElement.style.left = mousePt.x + 5 + "px";
		cxElement.style.top = mousePt.y + "px";
	}

	// Optional: Use a `window` click listener with event capture to
	//           remove the context menu if the user clicks elsewhere on the page
	window.addEventListener("click", hideContextMenu, true);
    }

  function hideContextMenu() {
    // This is the actual HTML context menu:
    var cxElement = document.getElementById("contextMenu");
    cxElement.classList.remove("show-menu");

    // Optional: Use a `window` click listener with event capture to
    //           remove the context menu if the user clicks elsewhere on the page
    window.removeEventListener("click", hideContextMenu, true);
  }

  // This is the general menu command handler, parameterized by the name of the command.
  function cxcommand(event, val) {
    if (val === undefined) val = event.currentTarget.id;
    var diagram = myDiagram;
    switch (val) {
      case "cut": diagram.commandHandler.cutSelection(); break;
      case "copy": diagram.commandHandler.copySelection(); break;
      case "paste": diagram.commandHandler.pasteSelection(diagram.lastInput.documentPoint); break;
      case "delete": diagram.commandHandler.deleteSelection(); break;
    }
    hideContextMenu();
  }
  </script>
</head>
<body onload="init()">
  <div id="myDiagramDiv" style="border: solid 1px black; width:100%; height:600px"></div>
	<ul id="contextMenu" class="menu">
		<li id="cut" class="menu-item" onclick="cxcommand(event)">Cut</li>
		<li id="copy" class="menu-item" onclick="cxcommand(event)">Copy</li>
		<li id="paste" class="menu-item" onclick="cxcommand(event)">Paste</li>
		<li id="delete" class="menu-item" onclick="cxcommand(event)">Delete</li>
  </ul>
</body>
</html>

I was never able to see the browser’s context menu.