npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

secure-electron-context-menu

v1.3.3

Published

A secure way to implement a context menu in electron apps.

Downloads

368

Readme

secure-electron-context-menu

A secure way to implement a context menu in electron apps. Create custom (or electron-defined) context menus. This package was designed to work within secure-electron-template.

Quality Gate Status Security Rating Maintainability Rating Bugs Vulnerabilities

Context menu

Getting started

Install via npm

Run npm i secure-electron-context-menu

Modify your main.js file

Modify the file that creates the BrowserWindow like so:

const {
  app,
  BrowserWindow,
  ipcMain,
  Menu,
  ...
} = require("electron");
const ContextMenu = require("secure-electron-context-menu").default;
const isDev = process.env.NODE_ENV === "development";

// Keep a global reference of the window object, if you don't, the window will
// be closed automatically when the JavaScript object is garbage collected.
let win;

async function createWindow() {

  // Create the browser window.
  win = new BrowserWindow({
    width: 800,
    height: 600,
    webPreferences: {
      contextIsolation: true,
      preload: path.join(__dirname, "preload.js") // a preload script is necessary!
    }
  });

  // Sets up bindings for our custom context menu
  ContextMenu.mainBindings(ipcMain, win, Menu, isDev, {
    "alertTemplate": [{
      id: "alert",
      label: "AN ALERT!"
    }]
  });

  // Load app
  win.loadFile("path_to_my_html_file");
}

// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.on("ready", createWindow);

app.on("window-all-closed", () => {
  // On macOS it is common for applications and their menu bar
  // to stay active until the user quits explicitly with Cmd + Q
  if (process.platform !== "darwin") {
    app.quit();
  } else {
    ContextMenu.clearMainBindings(ipcMain);
  }
});

Modify your preload.js file

Create/modify your existing preload file with the following additions:

const {
    contextBridge,
    ipcRenderer
} = require("electron");
const ContextMenu = require("secure-electron-context-menu").default;

// Expose protected methods that allow the renderer process to use
// the ipcRenderer without exposing the entire object
contextBridge.exposeInMainWorld(
    "api", {
        contextMenu: ContextMenu.preloadBindings(ipcRenderer)
    }
);

Defining your custom context menu

This library is unique in that we don't just give you the ability to use one context menu for your app, you have the power to make/use any number of context menus. Say, for instance that you want a different context menu to show up when you right-click a particular <div> than an <img> tag, you can do that!

In the .mainBindings call, you define all possible context menus for your app as the last parameter to the function. Each key can hold a custom array of menu items. You can see an example below where we have a more traditional context menu (with roles) and two custom context menus:

ContextMenu.mainBindings(ipcMain, win, Menu, isDev, {
    "alertTemplate": [{
      id: "alert",
      label: "ALERT ME!"
    }],
    "logTemplate": [{
        id: "log",
        label: "Log me"
      }, 
      {
        type: "separator"
      }, 
      {
        id: "calculate",
        label: "Open calculator"
    }],
    "default": [{
        label: "Edit",
        submenu: [
            {
                role: "undo"
            },
            {
                role: "redo"
            },
            {
                type: "separator"
            },
            {
                role: "cut"
            },
            {
                role: "copy"
            },
            {
                role: "paste"
            }
        ]
    }]
});

For any of the menu items that you'd like to take action on (in code), an id property is required. We'll see about more what that means in the next section.

Setting up an element to trigger the context menu

In order for your HTML elements to trigger a particular context menu, you need to add an cm-template attribute to it. For example:

<div cm-template="alertTemplate"></div>

Now, whenever this div is right-clicked, the "alertTemplate" context menu is shown. Additionally, if the isDev value passed into .mainBindings is true, an Inspect Element option is added to the context menu.

Passing custom values to the context menu

Showing a custom context menu is neat, but that isn't useful unless we can act on it somehow. Say, for example's sake, we want to allow the user to alert() a particular value from selecting an option from the context menu.

On any element that we have set an cm-template attribute, we can set any number of cm-payload- attributes to pass data we can act on when this context menu option is selected. An example:

import React from "react";
import "./contextmenu.css";

class Component extends React.Component {
  constructor(props) {
    super(props);
  }

  componentWillUnmount() {    
    // Clear any existing bindings;
    // important on mac-os if the app is suspended
    // and resumed. Existing subscriptions must be cleared
    window.api.contextMenu.clearRendererBindings();
  }

  componentDidMount() {
    // Set up binding in code whenever the context menu item
    // of id "alert" is selected
    window.api.contextMenu.onReceive("alert", function(args) {
            
      // We have access to the cm-payload-name value through:
      // args.attributes.name
      alert(args.attributes.name); // Alerts "abc"

      // An example showing you can pass more than one value
      console.log(args.attributes.name2); // Prints "def" in the console

      // Note - we have access to the "params" object as defined here: https://www.electronjs.org/docs/api/web-contents#event-context-menu
      // args.params
    });
  }

  render() {
    return (
      <div id="contextmenu">        
        <h1>Context menu</h1>
        <div cm-template="alertTemplate" cm-payload-name="abc" cm-payload-name2="def">
          Try right-clicking me for a custom context menu
        </div>
      </div>
    );
  }
}

export default Component;

What is needed is to create bindings in code using window.api.contextMenu.onReceive (as seen above) for each of the context menu items that you want to use in code. You can see that we have access to the attributes defined on the HTML.

This library works with plain JS too, and not just React!

It is also important to use the clearRendererBindings function as seen above, this is important on MacOS.

Context menus for items in a collection

If you are creating context menus for items in a collection, you need to add an cm-id attribute on your element and on the .onReceive listener, otherwise - the element you initiated the context menu with (ie., by right-clicking) may not be the element that receives the event!

It does not matter what the value of cm-id/onReceive event is, so long as it is unique between all elements that use the same template!

Assuming Sample is a component that you would render a collection of; instead of this:

import React from "react";

class Sample extends React.Component {
  constructor() {
    super();

    this.state = {
      name: "reZach"
    };

    this.changeName = this.changeName.bind(this);
  }

  componentWillUnmount() {
    window.api.contextMenu.clearRendererBindings();
  }

  componentDidMount() {
    window.api.contextMenu.onReceive(
      "log",
      function(args) {
        console.log(args.attributes.name);
      }.bind(this)
    );
  }

  changeName() {
    const names = ["Bob", "Jill", "Jane"];
    let newIndex = Math.floor(Math.random() * 3);
    this.setState((state) => ({
      name: names[newIndex]
    }));
  }

  render() {
    return (
      <div>
        <input
          type="button"
          onClick={this.changeName}
          value="Random name"></input>
        <div
          cm-template="logTemplate"
          cm-payload-name={this.state.name}>
          Right-click me for a custom context menu
        </div>
      </div>
    );
  }
}

Do this:

import React from "react";

class Sample extends React.Component {
  constructor() {
    super();

    this.state = {
      name: "reZach"
    };

    this.uniqueId = "Sample 1"; // In production apps, you'd make this unique per Sample (ie. use a Sample's id or a GUID)

    this.changeName = this.changeName.bind(this);
  }

  componentWillUnmount() {
    window.api.contextMenu.clearRendererBindings();
  }

  componentDidMount() {
    window.api.contextMenu.onReceive(
      "log",
      function(args) {
        console.log(args.attributes.name);
      }.bind(this),
      this.uniqueId /* added! */
    );
  }

  changeName() {
    const names = ["Bob", "Jill", "Jane"];
    let newIndex = Math.floor(Math.random() * 3);
    this.setState((state) => ({
      name: names[newIndex]
    }));
  }

  render() {
    return (
      <div>
        <input
          type="button"
          onClick={this.changeName}
          value="Random name"></input>
        <div
          cm-template="logTemplate"
          cm-id={this.uniqueId} {/* added! */}
          cm-payload-name={this.state.name}>
          Right-click me for a custom context menu
        </div>
      </div>
    );
  }
}