Contextual Menus in a Cross-Platform Electron Application

Context menu in Electron

Electron applications do not come with contextual menu out of the box. However, it is always a good idea to add a context menu to your project–it does bring more native look and feel. In this article, we are going to review different methods of contextual menu creation as well as building a dynamic menu with conditionally displayed items.

Adding a Generic Context Menu

As the first step of enhancing your electron application, let us take a look on the NPM module called electron-context-menu. You can install it by running the following command:

yarn add electron-context-menu

Initiate it in the main thread of your electron application like this:

const {app, BrowserWindow} = require('electron');
const contextMenu = require('electron-context-menu');

contextMenu({
	prepend: (params, browserWindow) => [{
		label: 'Rainbow',
		// Only show it when right-clicking images
		visible: params.mediaType === 'image'
	}]
});

Once initiated, your application will be provided with a standard contextual menu that has typical menu items that you would expect to find: copying text to clipboard, pasting it to text inputs, copying URLs, etc. There is also an option to enable Inspect Element item that opens Chrome developer tools with the clicked element selected. It may speed up the development, but do not forget to turn it off for production builds. Additionally, there’s a way to add custom menu items–dive into the module’s documentation to find out how.

Adding a Custom Context Menu

Once you add the aforementioned module, it will show the contextual menu on the right mouse click throughout the application. What if you want to disable this behavior for certain elements and show your customized contextual menu instead? That is actually super easy. To prevent the menu from showing up, you need to subscribe to the contextmenu event and call event.preventDefault();.

const element = document.getElementById('no-context-menu');
element.addEventListerener('contextmenu', (e) => {
    e.preventDefault();
});

After default action prevention, you can initiate your own contextual menu using the Electron’s Menu API. https://electronjs.org/docs/api/menu:

const {remote} = require('electron');
const {Menu, MenuItem} = remote;

const element = document.getElementById('custom-context-menu');
element.addEventListerener('contextmenu', (e) => {
    e.preventDefault();

    const menu = new Menu();

    menu.append(new MenuItem({
        label: gettextCatalog.getString('Chat'),
        click() {
            openChat();
        },
    }));

    // Append more menu items…

    menu.popup({window: remote.getCurrentWindow()});
    menu.once('menu-will-close', () => {
        menu.destroy();
    });
});

One little thing to point out is that since you re-creating the menu during each right click, you have to release it from memory once it gets hidden. Therefore we subscribe to the menu-will-close event and call the destroy() method on the menu instance.

Adding a Dynamic Context Menu

At last, let us consider the most interesting part–how to create a menu that has some specific items depending on the element that was clicked. For this purpose, we can abstract the menu creation login into a separate function–a menu builder–that may take some parameters.

The following function creates and shows a contextual menu for an assumed contact entity. It checks whether the given contact is chat-enabled, has unread messages or a phone number, and displays or hides the corresponding menu items.

function showContactContextMenu(contact) {
    const menu = new Menu();
    if (contact.hasUnreadMessages) {
        menu.append(new MenuItem({
            label: 'Mark as read',
            click() {
                markAllMessagesAsRead(contact);
            }
        }));
        menu.append(new MenuItem({type: 'separator'}));
    }
    if (contact.chatEnabled) {
        menu.append(new MenuItem({
            label: 'Chat',
            click() {
                openChat(contact);
            }
        }));
    }
    menu.append(new MenuItem({type: 'separator'}));
    if (contact.phoneNumber) {
        menu.append(new MenuItem({
            label: `Call (${contact.phoneNumber})`,
            click() {
                makeCall(contact.phoneNumber);
            }
        }));

        menu.append(new MenuItem({type: 'separator'}));
    }
    menu.append(new MenuItem({
        label: 'Profile',
        click() {
            showProfile(contact);
        }
    }));
    menu.append(new MenuItem({
        label: 'Delete',
        click() {
            deleteContact(contact);
        }
    }));
    menu.popup({window: remote.getCurrentWindow()});
    menu.once('menu-will-close', () => {
        menu.destroy();
    });
}

You may need multiple menu builder functions and use the strategy pattern in the event listener do determine which menu to show:

const {remote} = require('electron');
const {Menu, MenuItem} = remote;

const element = document.getElementById('entity-context-menu');
element.addEventListerener('contextmenu', (e) => {
    e.preventDefault();
    const entityType = element.getAttribute('data-entity-type');
    const entityId = element.getAttribute('data-entity-id');
    switch(entityType) {
        case 'contact':
            const contact = getContactById(entityId);
            showContactContextMenu(contact);
            break;
        case 'groupchat':
            const groupChat = getGroupChatById(entityId);
            showGroupChatContextMenu(groupChat);
            break;
        case 'voicemail':
            const voicemail = getVoicemailById(entityId);
            showVoiceMailContextMenu(voicemail);
            break;
    }
});

This contrived example assumes that you store necessary information in data attributes of HTML elements, but you can use any way you like–for instance, wrap this code into Angular directive and pass the entity object directly to it.

In this way, you can show different menus for different entities, as well as different menu items for a specific entity individually that are dependent on the entity’s features.

Hope, you have found this article useful and successfully managed to add a contextual menu to your Electron application. If you have any questions or suggestions, feel free to leave a comment below.