user2058239
user2058239

Reputation: 159

Disable Cut and Copy in context menu in Monaco editor

I am using monaco-editor, i see that cut and copy is added in the context menu in later versions. i want to remove these two options from context menu. Please let me know how can i achieve it?

Upvotes: 6

Views: 3843

Answers (6)

Danijel Sudar
Danijel Sudar

Reputation: 106

I faced the same challenge, and here's the solution that worked perfectly for me:

constructor(
  private renderer: Renderer2,
) {}

ngOnInit(): void {
  document.addEventListener('contextmenu', () => {
    setTimeout(() => {
      document.querySelectorAll('.action-item .action-label').forEach((item: HTMLElement) => {
        if (item.getAttribute('aria-label') === 'Copy') {
          this.renderer.setStyle(item.parentElement, 'display', 'none');
        }
      });
    }, 100);
  });
}

If this doesn't work for you, you can use the Inspect Elements tool to find the element's location in the context menu. To enable inspecting elements in the context menu, open the Developer Tool, press Ctrl + Shift + P, and select "Emulated a focused page." This will allow you to inspect elements within the context menu. Once you find the location of the text, you can try adjusting the code accordingly.

Upvotes: 1

Nightfall
Nightfall

Reputation: 127

Edit: this awnser is now outdated too, for the latest workarounds, check: https://github.com/microsoft/monaco-editor/issues/1567

The awnsers here are outdated, actions.MenuRegistry._menuItems seems to no longer return anything. The editor has also moved to a shadowroot breaking the css method.

The following works:

const shadowroot = document.querySelector(".shadow-root-host").shadowRoot
const RemoveContextMenuIndexes = [1, 2, 3, 4, 5, 6, 7, 8, 9, 12, 13];
for (const itemIndex of RemoveContextMenuIndexes) {
   shadowroot.querySelector(`ul.actions-container > li.action-item:nth-child(${itemIndex})`).style.display = "none";
}

If you have multiple editors, you will have more elements with the shadow-root-host class, thru which you would have to loop.

Upvotes: 1

Chris Newman
Chris Newman

Reputation: 3312

I ended up with a slightly modified approach based on Nighfalls answer

  • Tap into onContextMenu of the code editor. This will run when user opens the context menu. Before a user opens a context menu for the first time, shadow root does not exist in the DOM.
  • setTimeout is necessary here, in order for the querySelectors to find shadow root and context menu items, after they have been added. Again, because shadow root does not exist in the DOM until user has opened the context menu once.
  • Trying to set style.display on the action item element was causing typescript linting issues. .remove() on the element itself was causing a weird bug with hovering/highlighting action items further down in the menu. Ended up needing to .remove() the elements inside of the action item element.
this.codeEditorInstance.onContextMenu(() => {
  setTimeout(() => {
    const shadowroot = document.querySelector('.shadow-root-host')?.shadowRoot;
    if (shadowroot) {
      const RemoveContextMenuIndexes = [3];
      for (const itemIndex of RemoveContextMenuIndexes) {
        const el = shadowroot.querySelector(
          `ul.actions-container > li.action-item:nth-child(${itemIndex})`
        );
        if (el) {
          el.querySelector('a')
            ?.querySelectorAll('span')
            .forEach((s) => s.remove());
          el.querySelector('a')?.remove();
        }
      }
    }
  }, 0);
});

also worth mentioning that 'paste' is already removed in Firefox, which will affect which indexes you're trying to remove. so it's probably a good idea to check browser type, and remove indexes accordingly.

Upvotes: 0

Tobias Dummschat
Tobias Dummschat

Reputation: 1

Hide items without CSS

The following works for me and was adapted from this answer by KyleMit which did not work for me as stated.

import * as actions from 'monaco-editor/esm/vs/platform/actions/common/actions';

const idsToRemove = ['editor.action.clipboardCopyAction', 'editor.action.clipboardCutAction'];
actions.MenuRegistry._menuItems[1] = actions.MenuRegistry._menuItems[1]
        .filter(menuItem => !menuItem.command || !idsToRemove.includes(menuItem.command.id));

Upvotes: 0

KyleMit
KyleMit

Reputation: 30027

Full Code

import * as actions from "monaco-editor/esm/vs/platform/actions/common/actions";

let menus = actions.MenuRegistry._menuItems
let contextMenuEntry = [...menus].find(entry => entry[0]._debugName == "EditorContext")
let contextMenuLinks = contextMenuEntry[1]

let removableIds = ["editor.action.clipboardCopyAction", "editor.action.clipboardPasteAction"]

let removeById = (list, ids) => {
  let node = list._first
  do {
    let shouldRemove = ids.includes(node.element?.command?.id)
    if (shouldRemove) { list._remove(node) }
  } while ((node = node.next))
}

removeById(contextMenuLinks, removableIds)

Walkthrough

You can access the available menu functions from MenuRegistry inside actions.js:

import * as actions from "monaco-editor/esm/vs/platform/actions/common/actions"
let menus = actions.MenuRegistry._menuItems

This will provide a list of all menu types: i.e.
["MenubarEditMenu", "CommandPalette", "EditorContext", ...]

To access and modify the context menu specifically, we can find it in the menu map:

let contextMenuEntry = [...menus].find(entry => entry[0]._debugName == "EditorContext")
let contextMenuLinks = contextMenuEntry[1]

The menu items are of type LinkedList, where each node contains an element and a reference to the prev and next node, but it comes with some utility methods that make it easier to reason about.

So if you want to list all commands, you can do this:

let allCommandIds = [...contextMenuLinks].map(el => el.command?.id)

Using that, identify the list of commands you want to pluck out ahead of time - in our case:

let removableIds = [
  "editor.action.clipboardCopyAction",
  "editor.action.clipboardPasteAction",
]

Next we need to identify and remove the nodes with those ids. The iterator returns the node.element, but the _remove() function takes in the entire node, so we'll have to iterate a little different than before. Here's a function that loops through all nodes and removes each if

We'll then get all the nodes we want to remove:

let removeById = (list, ids) => {
  let node = list._first;
  do {
    let shouldRemove = ids.includes(node.element?.command?.id)
    if (shouldRemove) { list._remove(node) }
  } while ((node = node.next))
}

And then call like this:

removeById(contextMenuLinks, removableIds)

Demo

Screenshot Example

Further Reading

Upvotes: 3

T04435
T04435

Reputation: 14002

Hide individual items with CSS

I tried this code in the browser and it worked.

// Hide from cut on
.context-view li.action-item:nth-child(n + 9) {
    display: none !important;
}

// Show command palette
.context-view li.action-item:last-child {
  display: flex !important;
}

Disable the entire menu with API

monacoOptions = {
  // other options
  contextmenu: false
}

See Docs on IEditorConstructionOptions > contextmenu

Upvotes: 2

Related Questions