├── .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 | [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=reZach_secure-electron-context-menu&metric=alert_status)](https://sonarcloud.io/dashboard?id=reZach_secure-electron-context-menu) 5 | [![Security Rating](https://sonarcloud.io/api/project_badges/measure?project=reZach_secure-electron-context-menu&metric=security_rating)](https://sonarcloud.io/dashboard?id=reZach_secure-electron-context-menu) 6 | [![Maintainability Rating](https://sonarcloud.io/api/project_badges/measure?project=reZach_secure-electron-context-menu&metric=sqale_rating)](https://sonarcloud.io/dashboard?id=reZach_secure-electron-context-menu) 7 | [![Bugs](https://sonarcloud.io/api/project_badges/measure?project=reZach_secure-electron-context-menu&metric=bugs)](https://sonarcloud.io/dashboard?id=reZach_secure-electron-context-menu) 8 | [![Vulnerabilities](https://sonarcloud.io/api/project_badges/measure?project=reZach_secure-electron-context-menu&metric=vulnerabilities)](https://sonarcloud.io/dashboard?id=reZach_secure-electron-context-menu) 9 | 10 | ![Context menu](https://github.com/reZach/secure-electron-context-menu/blob/master/docs/contextmenu.png "Context menu") 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 |
265 |
266 | ); 267 | } 268 | } 269 | ``` 270 | 271 | Do this: 272 | ```jsx 273 | import React from "react"; 274 | 275 | class Sample extends React.Component { 276 | constructor() { 277 | super(); 278 | 279 | this.state = { 280 | name: "reZach" 281 | }; 282 | 283 | this.uniqueId = "Sample 1"; // In production apps, you'd make this unique per Sample (ie. use a Sample's id or a GUID) 284 | 285 | this.changeName = this.changeName.bind(this); 286 | } 287 | 288 | componentWillUnmount() { 289 | window.api.contextMenu.clearRendererBindings(); 290 | } 291 | 292 | componentDidMount() { 293 | window.api.contextMenu.onReceive( 294 | "log", 295 | function(args) { 296 | console.log(args.attributes.name); 297 | }.bind(this), 298 | this.uniqueId /* added! */ 299 | ); 300 | } 301 | 302 | changeName() { 303 | const names = ["Bob", "Jill", "Jane"]; 304 | let newIndex = Math.floor(Math.random() * 3); 305 | this.setState((state) => ({ 306 | name: names[newIndex] 307 | })); 308 | } 309 | 310 | render() { 311 | return ( 312 |
313 | 317 |
321 | Right-click me for a custom context menu 322 |
323 |
324 | ); 325 | } 326 | } 327 | 328 | ``` -------------------------------------------------------------------------------- /docs/contextmenu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reZach/secure-electron-context-menu/cf96f2ff57f3eca36a220c38cbcb117d0b63fb38/docs/contextmenu.png -------------------------------------------------------------------------------- /lib/index.d.ts: -------------------------------------------------------------------------------- 1 | export default contextMenu; 2 | declare const contextMenu: ContextMenu; 3 | declare class ContextMenu { 4 | constructor(options: any); 5 | options: any; 6 | selectedElement: any; 7 | selectedElementAttributes: {}; 8 | contextMenuParams: {}; 9 | stagedInternalFnMap: {}; 10 | internalFnMap: {}; 11 | cleanedTemplates: {}; 12 | findContextElement(element: any, x: any, y: any): any; 13 | preloadBindings(ipcRenderer: any): { 14 | onReceive: (menuActionId: any, func: any, id: any) => void; 15 | clearRendererBindings: () => void; 16 | }; 17 | id: any; 18 | mainBindings(ipcMain: any, browserWindow: any, Menu: any, isDevelopment: any, templates: any): void; 19 | clearMainBindings(ipcMain: any): void; 20 | } 21 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | "use strict";var _lodash=require("lodash");Object.defineProperty(exports,"__esModule",{value:!0}),exports["default"]=void 0;function _createForOfIteratorHelper(a,b){var c="undefined"!=typeof Symbol&&a[Symbol.iterator]||a["@@iterator"];if(!c){if(Array.isArray(a)||(c=_unsupportedIterableToArray(a))||b&&a&&"number"==typeof a.length){c&&(a=c);var d=0,e=function(){};return{s:e,n:function n(){return d>=a.length?{done:!0}:{done:!1,value:a[d++]}},e:function e(a){throw a},f:e}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var f,g=!0,h=!1;return{s:function s(){c=c.call(a)},n:function n(){var a=c.next();return g=a.done,a},e:function e(a){h=!0,f=a},f:function f(){try{g||null==c["return"]||c["return"]()}finally{if(h)throw f}}}}function _unsupportedIterableToArray(a,b){if(a){if("string"==typeof a)return _arrayLikeToArray(a,b);var c=Object.prototype.toString.call(a).slice(8,-1);return"Object"===c&&a.constructor&&(c=a.constructor.name),"Map"===c||"Set"===c?Array.from(a):"Arguments"===c||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(c)?_arrayLikeToArray(a,b):void 0}}function _arrayLikeToArray(a,b){(null==b||b>a.length)&&(b=a.length);for(var c=0,d=Array(b);cg||ch)return null;var i=a.getAttribute(this.options.templateAttributeName);return""!==i&&null!==i?a:this.findContextElement(a.parentElement,b,c)}},{key:"preloadBindings",value:function preloadBindings(a){var b=this,c=function(){b.id="",a.on(contextMenuRequest,function(c,d){var e=null;if(b.selectedElement=null,b.selectedElementAttributes={},b.contextMenuParams=d.params,b.selectedElement=b.findContextElement(document.elementFromPoint(d.params.x,d.params.y),d.params.x,d.params.y),null!==b.selectedElement){var i=b.selectedElement.getAttribute(b.options.templateAttributeName);if(""!==i&&null!==i){var f,g=b.selectedElement.attributes,h=_createForOfIteratorHelper(g);try{for(h.s();!(f=h.n()).done;){var j=f.value;0<=j.name.indexOf(b.options.payloadAttributeName)?b.selectedElementAttributes[j.name.replace("".concat(b.options.payloadAttributeName,"-"),"")]=j.value:0<=j.name.indexOf(b.options.idAttributeName)&&(b.id=j.value)}}catch(a){h.e(a)}finally{h.f()}e=i}}a.send(contextMenuResponse,{id:b.id,params:d.params,template:e})}),a.on(contextMenuClicked,function(a,c){if("undefined"!=typeof b.internalFnMap[c.id]){var d={params:b.contextMenuParams,attributes:b.selectedElementAttributes};b.internalFnMap[c.id](d)}})};return c(),{onReceive:function onReceive(a,c,d){"undefined"==typeof d?b.internalFnMap[a]=c:b.internalFnMap["".concat(d,"___").concat(a)]=c},clearRendererBindings:function clearRendererBindings(){b.stagedInternalFnMap={},b.internalFnMap={},b.contextMenuParams={},a.removeAllListeners(contextMenuRequest),a.removeAllListeners(contextMenuClicked),c()}}}},{key:"mainBindings",value:function mainBindings(a,b,c,d,e){var f=this;b.webContents.on("context-menu",function(a,c){b.webContents.send(contextMenuRequest,{params:c})}),a.on(contextMenuResponse,function(a,g){var h,i=g.id?"".concat(g.id,"___"):"",j="".concat(i).concat(g.template);if(null===g.template||"undefined"==typeof f.cleanedTemplates[j]){if(h=e[g.template]?(0,_lodash.cloneDeep)(e[g.template]):[],d&&h.push({label:"Inspect element",click:function click(){b.inspectElement(g.params.x,g.params.y)}}),null!==g.template){var k,l=_createForOfIteratorHelper(h);try{var m=function(){var a=k.value;"undefined"==typeof a.click&&(a.click=function(){b.webContents.send(contextMenuClicked,{id:"".concat(i).concat(a.id||a.label)})})};for(l.s();!(k=l.n()).done;)m()}catch(a){l.e(a)}finally{l.f()}}f.cleanedTemplates[j]=h}h=f.cleanedTemplates[j],c.buildFromTemplate(h).popup(b)})}},{key:"clearMainBindings",value:function clearMainBindings(a){this.cleanedTemplates={},a.removeAllListeners(contextMenuResponse)}}]),a}(),contextMenu=new ContextMenu,_default=contextMenu;exports["default"]=_default; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "secure-electron-context-menu", 3 | "version": "1.3.4", 4 | "description": "A secure way to implement a context menu in electron apps.", 5 | "main": "lib/index.js", 6 | "types": "lib/index.d.ts", 7 | "scripts": { 8 | "build": "npm run genTypes & npx babel ./src -d ./lib", 9 | "genTypes": "tsc src/*.js --declaration --allowJs --emitDeclarationOnly --outDir ./lib" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/reZach/secure-electron-context-menu.git" 14 | }, 15 | "keywords": [ 16 | "context", 17 | "menu", 18 | "electron", 19 | "secure" 20 | ], 21 | "author": "reZach", 22 | "license": "MIT", 23 | "bugs": { 24 | "url": "https://github.com/reZach/secure-electron-context-menu/issues" 25 | }, 26 | "homepage": "https://github.com/reZach/secure-electron-context-menu#readme", 27 | "devDependencies": { 28 | "@babel/cli": "^7.15.4", 29 | "@babel/core": "^7.15.5", 30 | "@babel/preset-env": "^7.15.6", 31 | "babel-preset-minify": "^0.5.1", 32 | "typescript": "^5.7.2" 33 | }, 34 | "dependencies": { 35 | "lodash.clonedeep": "^4.5.0" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import { cloneDeep } from "lodash"; 2 | 3 | const defaultOptions = { 4 | templateAttributeName: "cm-template", 5 | payloadAttributeName: "cm-payload", 6 | idAttributeName: "cm-id" 7 | }; 8 | 9 | // Electron-specific; must match between main/renderer ipc 10 | const contextMenuRequest = "ContextMenu-Request"; 11 | const contextMenuResponse = "ContextMenu-Response"; 12 | const contextMenuClicked = "ContextMenu-Clicked"; 13 | 14 | class ContextMenu { 15 | constructor(options) { 16 | this.options = defaultOptions; 17 | this.selectedElement = null; 18 | this.selectedElementAttributes = {}; 19 | this.contextMenuParams = {}; 20 | this.stagedInternalFnMap = {}; 21 | this.internalFnMap = {}; 22 | this.cleanedTemplates = {}; 23 | 24 | // Merge any options the user passed in 25 | if (typeof options !== "undefined") { 26 | this.options = Object.assign(this.options, options); 27 | } 28 | } 29 | 30 | findContextElement(element, x, y) { 31 | if (!element) { 32 | return null; 33 | } 34 | 35 | // Check if current cursor is inside element 36 | const { top, left, right, bottom } = element.getBoundingClientRect(); 37 | if (x < left || x > right || y < top || y > bottom) { 38 | return null; 39 | } 40 | 41 | // Check if it has context value 42 | const contextTemplate = element.getAttribute(this.options.templateAttributeName) 43 | if (contextTemplate !== "" && contextTemplate !== null) { 44 | return element; 45 | } 46 | 47 | return this.findContextElement(element.parentElement, x, y); 48 | } 49 | 50 | preloadBindings(ipcRenderer) { 51 | 52 | const createIpcBindings = () => { 53 | this.id = ""; 54 | 55 | ipcRenderer.on(contextMenuRequest, (event, args) => { 56 | 57 | // Reset 58 | let templateToSend = null; 59 | this.selectedElement = null; 60 | this.selectedElementAttributes = {}; 61 | this.contextMenuParams = args.params; 62 | 63 | // Grab the element where the user clicked 64 | this.selectedElement = this.findContextElement(document.elementFromPoint(args.params.x, args.params.y), args.params.x, args.params.y); 65 | if (this.selectedElement !== null) { 66 | 67 | const contextMenuTemplate = this.selectedElement.getAttribute(this.options.templateAttributeName); 68 | if (contextMenuTemplate !== "" && contextMenuTemplate !== null) { 69 | 70 | // Save all attribute values for later-use when 71 | // we call the callback defined for this context menu item 72 | const attributes = this.selectedElement.attributes; 73 | for (const attribute of attributes) 74 | { 75 | if (attribute.name.indexOf(this.options.payloadAttributeName) >= 0) { 76 | this.selectedElementAttributes[attribute.name.replace(`${this.options.payloadAttributeName}-`, "")] = attribute.value; 77 | } else if (attribute.name.indexOf(this.options.idAttributeName) >= 0) { 78 | this.id = attribute.value; 79 | } 80 | } 81 | 82 | templateToSend = contextMenuTemplate; 83 | } 84 | } 85 | 86 | // Send the request to the main process; 87 | // so the menu can get built 88 | ipcRenderer.send(contextMenuResponse, { 89 | id: this.id, 90 | params: args.params, 91 | template: templateToSend 92 | }); 93 | }); 94 | 95 | ipcRenderer.on(contextMenuClicked, (event, args) => { 96 | if (typeof this.internalFnMap[args.id] !== "undefined") { 97 | const payload = { 98 | params: this.contextMenuParams, 99 | attributes: this.selectedElementAttributes 100 | }; 101 | this.internalFnMap[args.id](payload); 102 | } 103 | }); 104 | }; 105 | createIpcBindings(); 106 | 107 | return { 108 | onReceive: (menuActionId, func, id) => { 109 | if (typeof id === "undefined") { 110 | this.internalFnMap[menuActionId] = func; 111 | } else { 112 | this.internalFnMap[`${id}___${menuActionId}`] = func; 113 | } 114 | }, 115 | clearRendererBindings: () => { 116 | this.stagedInternalFnMap = {}; 117 | this.internalFnMap = {}; 118 | this.contextMenuParams = {}; 119 | ipcRenderer.removeAllListeners(contextMenuRequest); 120 | ipcRenderer.removeAllListeners(contextMenuClicked); 121 | createIpcBindings(); 122 | } 123 | } 124 | } 125 | 126 | mainBindings(ipcMain, browserWindow, Menu, isDevelopment, templates) { 127 | 128 | // Anytime a user right-clicks the browser window, send where they 129 | // clicked to the renderer process 130 | browserWindow.webContents.on("context-menu", (event, params) => { 131 | browserWindow.webContents.send(contextMenuRequest, { 132 | params 133 | }); 134 | }); 135 | 136 | ipcMain.on(contextMenuResponse, (IpcMainEvent, args) => { 137 | 138 | // id prepend; if we have a list of common elements, 139 | // certain bindings may not work because each element would have 140 | // registered for the same event name. In these cases, prepend each 141 | // menu item with the unique id passed in so that each individual 142 | // component can respond appropriately to the context menu action 143 | const idPrepend = args.id ? `${args.id}___` : ""; 144 | const cleanedTemplatesKey = `${idPrepend}${args.template}`; 145 | 146 | let generatedContextMenu; 147 | if (args.template === null || typeof this.cleanedTemplates[cleanedTemplatesKey] === "undefined") { 148 | 149 | // Build our context menu based on our templates 150 | generatedContextMenu = templates[args.template] ? cloneDeep(templates[args.template]) : []; 151 | if (isDevelopment) { 152 | generatedContextMenu.push({ 153 | label: "Inspect element", 154 | click: () => { 155 | browserWindow.inspectElement(args.params.x, args.params.y); 156 | } 157 | }); 158 | } 159 | 160 | if (args.template !== null) { 161 | 162 | // For any menu items that don't have a role or click event, 163 | // create one so we can tie back the click to the code! 164 | for (let menu of generatedContextMenu) 165 | { 166 | if (typeof menu["click"] === "undefined") { 167 | menu.click = function (event, window, webContents) { 168 | browserWindow.webContents.send(contextMenuClicked, { 169 | id: `${idPrepend}${(menu.id || menu.label)}` 170 | }); 171 | } 172 | } 173 | } 174 | } 175 | 176 | // Save this cleaned template, so we can re-use it 177 | this.cleanedTemplates[cleanedTemplatesKey] = generatedContextMenu; 178 | } 179 | generatedContextMenu = this.cleanedTemplates[cleanedTemplatesKey]; 180 | 181 | Menu.buildFromTemplate(generatedContextMenu).popup(browserWindow); 182 | }); 183 | } 184 | 185 | clearMainBindings(ipcMain) { 186 | this.cleanedTemplates = {}; 187 | ipcMain.removeAllListeners(contextMenuResponse); 188 | } 189 | } 190 | 191 | const contextMenu = new ContextMenu(); 192 | export default contextMenu; --------------------------------------------------------------------------------