├── .babelrc
├── .gitignore
├── .npmignore
├── LICENSE
├── README.md
├── docs
└── contextmenu.png
├── lib
├── index.d.ts
└── index.js
├── package-lock.json
├── package.json
└── src
└── index.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | "@babel/preset-env",
4 | "minify"
5 | ],
6 | "comments": false
7 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | lerna-debug.log*
8 |
9 | # Diagnostic reports (https://nodejs.org/api/report.html)
10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
11 |
12 | # Runtime data
13 | pids
14 | *.pid
15 | *.seed
16 | *.pid.lock
17 |
18 | # Directory for instrumented libs generated by jscoverage/JSCover
19 | lib-cov
20 |
21 | # Coverage directory used by tools like istanbul
22 | coverage
23 | *.lcov
24 |
25 | # nyc test coverage
26 | .nyc_output
27 |
28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
29 | .grunt
30 |
31 | # Bower dependency directory (https://bower.io/)
32 | bower_components
33 |
34 | # node-waf configuration
35 | .lock-wscript
36 |
37 | # Compiled binary addons (https://nodejs.org/api/addons.html)
38 | build/Release
39 |
40 | # Dependency directories
41 | node_modules/
42 | jspm_packages/
43 |
44 | # TypeScript v1 declaration files
45 | typings/
46 |
47 | # TypeScript cache
48 | *.tsbuildinfo
49 |
50 | # Optional npm cache directory
51 | .npm
52 |
53 | # Optional eslint cache
54 | .eslintcache
55 |
56 | # Microbundle cache
57 | .rpt2_cache/
58 | .rts2_cache_cjs/
59 | .rts2_cache_es/
60 | .rts2_cache_umd/
61 |
62 | # Optional REPL history
63 | .node_repl_history
64 |
65 | # Output of 'npm pack'
66 | *.tgz
67 |
68 | # Yarn Integrity file
69 | .yarn-integrity
70 |
71 | # dotenv environment variables file
72 | .env
73 | .env.test
74 |
75 | # parcel-bundler cache (https://parceljs.org/)
76 | .cache
77 |
78 | # Next.js build output
79 | .next
80 |
81 | # Nuxt.js build / generate output
82 | .nuxt
83 | dist
84 |
85 | # Gatsby files
86 | .cache/
87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js
88 | # https://nextjs.org/blog/next-9-1#public-directory-support
89 | # public
90 |
91 | # vuepress build output
92 | .vuepress/dist
93 |
94 | # Serverless directories
95 | .serverless/
96 |
97 | # FuseBox cache
98 | .fusebox/
99 |
100 | # DynamoDB Local files
101 | .dynamodb/
102 |
103 | # TernJS port file
104 | .tern-port
105 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | README.md
3 | src/
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 reZach
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # secure-electron-context-menu
2 | 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`](https://github.com/reZach/secure-electron-template).
3 |
4 | [](https://sonarcloud.io/dashboard?id=reZach_secure-electron-context-menu)
5 | [](https://sonarcloud.io/dashboard?id=reZach_secure-electron-context-menu)
6 | [](https://sonarcloud.io/dashboard?id=reZach_secure-electron-context-menu)
7 | [](https://sonarcloud.io/dashboard?id=reZach_secure-electron-context-menu)
8 | [](https://sonarcloud.io/dashboard?id=reZach_secure-electron-context-menu)
9 |
10 | 
11 |
12 | ## Getting started
13 |
14 | ### Install via npm
15 | Run `npm i secure-electron-context-menu`
16 |
17 | ### Modify your main.js file
18 | Modify the file that creates the [`BrowserWindow`](https://www.electronjs.org/docs/api/browser-window) like so:
19 |
20 | ```javascript
21 | const {
22 | app,
23 | BrowserWindow,
24 | ipcMain,
25 | Menu,
26 | ...
27 | } = require("electron");
28 | const ContextMenu = require("secure-electron-context-menu").default;
29 | const isDev = process.env.NODE_ENV === "development";
30 |
31 | // Keep a global reference of the window object, if you don't, the window will
32 | // be closed automatically when the JavaScript object is garbage collected.
33 | let win;
34 |
35 | async function createWindow() {
36 |
37 | // Create the browser window.
38 | win = new BrowserWindow({
39 | width: 800,
40 | height: 600,
41 | webPreferences: {
42 | contextIsolation: true,
43 | preload: path.join(__dirname, "preload.js") // a preload script is necessary!
44 | }
45 | });
46 |
47 | // Sets up bindings for our custom context menu
48 | ContextMenu.mainBindings(ipcMain, win, Menu, isDev, {
49 | "alertTemplate": [{
50 | id: "alert",
51 | label: "AN ALERT!"
52 | }]
53 | });
54 |
55 | // Load app
56 | win.loadFile("path_to_my_html_file");
57 | }
58 |
59 | // This method will be called when Electron has finished
60 | // initialization and is ready to create browser windows.
61 | // Some APIs can only be used after this event occurs.
62 | app.on("ready", createWindow);
63 |
64 | app.on("window-all-closed", () => {
65 | // On macOS it is common for applications and their menu bar
66 | // to stay active until the user quits explicitly with Cmd + Q
67 | if (process.platform !== "darwin") {
68 | app.quit();
69 | } else {
70 | ContextMenu.clearMainBindings(ipcMain);
71 | }
72 | });
73 | ```
74 |
75 |
76 | ### Modify your preload.js file
77 | Create/modify your existing preload file with the following additions:
78 | ```javascript
79 | const {
80 | contextBridge,
81 | ipcRenderer
82 | } = require("electron");
83 | const ContextMenu = require("secure-electron-context-menu").default;
84 |
85 | // Expose protected methods that allow the renderer process to use
86 | // the ipcRenderer without exposing the entire object
87 | contextBridge.exposeInMainWorld(
88 | "api", {
89 | contextMenu: ContextMenu.preloadBindings(ipcRenderer)
90 | }
91 | );
92 | ```
93 |
94 | ## Defining your custom context menu
95 | 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!
96 |
97 | 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](https://www.electronjs.org/docs/api/menu-item). You can see an example below where we have a more traditional context menu (with [roles](https://www.electronjs.org/docs/api/menu-item#roles)) and two custom context menus:
98 |
99 | ```javascript
100 | ContextMenu.mainBindings(ipcMain, win, Menu, isDev, {
101 | "alertTemplate": [{
102 | id: "alert",
103 | label: "ALERT ME!"
104 | }],
105 | "logTemplate": [{
106 | id: "log",
107 | label: "Log me"
108 | },
109 | {
110 | type: "separator"
111 | },
112 | {
113 | id: "calculate",
114 | label: "Open calculator"
115 | }],
116 | "default": [{
117 | label: "Edit",
118 | submenu: [
119 | {
120 | role: "undo"
121 | },
122 | {
123 | role: "redo"
124 | },
125 | {
126 | type: "separator"
127 | },
128 | {
129 | role: "cut"
130 | },
131 | {
132 | role: "copy"
133 | },
134 | {
135 | role: "paste"
136 | }
137 | ]
138 | }]
139 | });
140 | ```
141 |
142 | 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.
143 |
144 | ## Setting up an element to trigger the context menu
145 | In order for your HTML elements to trigger a particular context menu, you need to add an `cm-template` attribute to it. For example:
146 |
147 | ```html
148 |
149 | ```
150 |
151 | 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.
152 |
153 | ## Passing custom values to the context menu
154 | 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.
155 |
156 | 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:
157 |
158 | ```jsx
159 | import React from "react";
160 | import "./contextmenu.css";
161 |
162 | class Component extends React.Component {
163 | constructor(props) {
164 | super(props);
165 | }
166 |
167 | componentWillUnmount() {
168 | // Clear any existing bindings;
169 | // important on mac-os if the app is suspended
170 | // and resumed. Existing subscriptions must be cleared
171 | window.api.contextMenu.clearRendererBindings();
172 | }
173 |
174 | componentDidMount() {
175 | // Set up binding in code whenever the context menu item
176 | // of id "alert" is selected
177 | window.api.contextMenu.onReceive("alert", function(args) {
178 |
179 | // We have access to the cm-payload-name value through:
180 | // args.attributes.name
181 | alert(args.attributes.name); // Alerts "abc"
182 |
183 | // An example showing you can pass more than one value
184 | console.log(args.attributes.name2); // Prints "def" in the console
185 |
186 | // Note - we have access to the "params" object as defined here: https://www.electronjs.org/docs/api/web-contents#event-context-menu
187 | // args.params
188 | });
189 | }
190 |
191 | render() {
192 | return (
193 |
194 |
Context menu
195 |
196 | Try right-clicking me for a custom context menu
197 |
198 |
199 | );
200 | }
201 | }
202 |
203 | export default Component;
204 | ```
205 |
206 | 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.
207 |
208 | > This library works with plain JS too, and not just React!
209 |
210 | It is also important to use the `clearRendererBindings` function as seen above, this is important on MacOS.
211 |
212 | ## Context menus for items in a collection
213 | 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!
214 |
215 | > 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!
216 |
217 | Assuming `Sample` is a component that you would render a collection of; instead of this:
218 | ```jsx
219 | import React from "react";
220 |
221 | class Sample extends React.Component {
222 | constructor() {
223 | super();
224 |
225 | this.state = {
226 | name: "reZach"
227 | };
228 |
229 | this.changeName = this.changeName.bind(this);
230 | }
231 |
232 | componentWillUnmount() {
233 | window.api.contextMenu.clearRendererBindings();
234 | }
235 |
236 | componentDidMount() {
237 | window.api.contextMenu.onReceive(
238 | "log",
239 | function(args) {
240 | console.log(args.attributes.name);
241 | }.bind(this)
242 | );
243 | }
244 |
245 | changeName() {
246 | const names = ["Bob", "Jill", "Jane"];
247 | let newIndex = Math.floor(Math.random() * 3);
248 | this.setState((state) => ({
249 | name: names[newIndex]
250 | }));
251 | }
252 |
253 | render() {
254 | return (
255 |
256 |
260 |
263 | Right-click me for a custom context menu
264 |